sanity-plugin-hotspot-array 0.0.7 → 0.1.0-v3-studio.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/package.json CHANGED
@@ -1,75 +1,89 @@
1
1
  {
2
2
  "name": "sanity-plugin-hotspot-array",
3
- "version": "0.0.7",
3
+ "version": "0.1.0-v3-studio.1",
4
4
  "description": "A configurable Custom Input for Arrays that will add and update items by clicking on an Image",
5
- "main": "lib/HotspotArray.js",
6
- "scripts": {
7
- "build": "sanipack build",
8
- "watch": "sanipack build --watch",
9
- "_postinstall": "husky install",
10
- "prepublishOnly": "pinst --disable && sanipack build && sanipack verify",
11
- "postpublish": "pinst --enable",
12
- "lint": "eslint .",
13
- "lint:fix": "eslint . --fix"
14
- },
15
- "husky": {
16
- "hooks": {
17
- "pre-commit": "npm run lint:fix"
5
+ "author": "Simeon Griggs <simeon@sanity.io>",
6
+ "license": "MIT",
7
+ "main": "./lib/cjs/index.js",
8
+ "source": "./src/index.tsx",
9
+ "module": "./lib/esm/index.js",
10
+ "types": "./lib/types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "require": "./lib/cjs/index.js",
14
+ "default": "./lib/esm/index.js"
18
15
  }
19
16
  },
17
+ "files": [
18
+ "src",
19
+ "lib",
20
+ "v2-incompatible.js",
21
+ "sanity.json"
22
+ ],
23
+ "engines": {
24
+ "node": ">=14.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "parcel build --no-cache",
28
+ "watch": "parcel watch",
29
+ "prepublishOnly": "npm run build",
30
+ "lint": "eslint src/**/*",
31
+ "lint:fix": "eslint . --fix",
32
+ "clean": "rimraf lib",
33
+ "prebuild": "npm run clean && plugin-kit verify-package --silent",
34
+ "link-watch": "plugin-kit link-watch",
35
+ "prepare": "husky install"
36
+ },
20
37
  "repository": {
21
38
  "type": "git",
22
- "url": "git+ssh://git@github.com/SimeonGriggs/sanity-plugin-hotspot-array.git"
39
+ "url": "git@github.com:sanity-io/sanity-plugin-hotspot-array.git"
23
40
  },
24
41
  "keywords": [
25
42
  "sanity",
26
43
  "sanity-plugin"
27
44
  ],
28
- "author": "Simeon Griggs <simeon@sanity.io>",
29
- "license": "MIT",
30
45
  "dependencies": {
31
46
  "@react-hookz/web": "^14.2.2",
32
47
  "@sanity/asset-utils": "^1.2.3",
33
- "@sanity/base": "^2.30.1",
34
- "@sanity/form-builder": "^2.30.1",
35
- "@sanity/ui": "^0.37.12",
36
- "framer-motion": "^6.3.11",
37
- "husky": "^8.0.1"
48
+ "@sanity/incompatible-plugin": "^1.0.4",
49
+ "@sanity/ui": "^1.0.0-beta.31",
50
+ "@sanity/util": "dev-preview || 3.0.0-dev-preview.22",
51
+ "framer-motion": "^6.3.11"
38
52
  },
39
53
  "devDependencies": {
40
- "eslint": "^8.17.0",
54
+ "@commitlint/cli": "^17.1.2",
55
+ "@commitlint/config-conventional": "^17.1.0",
56
+ "@parcel/packager-ts": "^2.6.2",
57
+ "@parcel/transformer-typescript-types": "^2.6.2",
58
+ "@sanity/plugin-kit": "^1.0.2",
59
+ "@sanity/semantic-release-preset": "^2.0.2",
60
+ "@typescript-eslint/eslint-plugin": "^5.41.0",
61
+ "@typescript-eslint/parser": "^5.41.0",
62
+ "eslint": "^8.26.0",
41
63
  "eslint-config-prettier": "^8.5.0",
42
64
  "eslint-config-sanity": "^6.0.0",
43
- "eslint-plugin-react": "^7.30.0",
65
+ "eslint-plugin-prettier": "^4.2.1",
66
+ "eslint-plugin-react": "^7.31.10",
67
+ "eslint-plugin-react-hooks": "^4.6.0",
68
+ "husky": "^8.0.1",
69
+ "lint-staged": "^13.0.3",
70
+ "parcel": "~2.6.0",
44
71
  "pinst": "^2.1.6",
45
- "prettier": "^2.7.0",
46
- "sanipack": "^2.1.0"
72
+ "prettier": "^2.7.1",
73
+ "react": "^18.0.0",
74
+ "rimraf": "^3.0.2",
75
+ "sanipack": "^2.1.0",
76
+ "sanity": "dev-preview || 3.0.0-dev-preview.22",
77
+ "typescript": "~4.7.0"
47
78
  },
48
79
  "peerDependencies": {
49
80
  "@sanity/image-url": "1.0.1",
50
- "@sanity/util": "2.29.5",
51
81
  "lodash": "4.17.21",
52
- "react": "^17.0.2"
82
+ "react": "^18.0.0",
83
+ "sanity": "dev-preview || 3.0.0-dev-preview.22"
53
84
  },
54
85
  "bugs": {
55
- "url": "https://github.com/SimeonGriggs/sanity-plugin-hotspot-array/issues"
56
- },
57
- "homepage": "https://github.com/SimeonGriggs/sanity-plugin-hotspot-array#readme",
58
- "prettier": {
59
- "semi": false,
60
- "printWidth": 100,
61
- "bracketSpacing": false,
62
- "singleQuote": true
86
+ "url": "https://github.com/sanity-io/sanity-plugin-hotspot-array/issues"
63
87
  },
64
- "eslintConfig": {
65
- "parser": "sanipack/babel/eslint-parser",
66
- "extends": [
67
- "sanity",
68
- "sanity/react",
69
- "prettier"
70
- ],
71
- "ignorePatterns": [
72
- "lib/**/"
73
- ]
74
- }
88
+ "homepage": "https://github.com/sanity-io/sanity-plugin-hotspot-array#readme"
75
89
  }
package/sanity.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
- "paths": {
3
- "source": "./src",
4
- "compiled": "./lib"
5
- },
6
- "parts": []
2
+ "parts": [
3
+ {
4
+ "implements": "part:@sanity/base/sanity-root",
5
+ "path": "./v2-incompatible.js"
6
+ }
7
+ ]
7
8
  }
package/src/Feedback.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import React from 'react'
1
+ import React, {ReactNode} from 'react'
2
2
  import {Card, Text} from '@sanity/ui'
3
3
 
4
- export default function Feedback({children}) {
4
+ export default function Feedback({children}: {children: ReactNode}) {
5
5
  return (
6
6
  <Card padding={4} radius={2} shadow={1} tone="caution">
7
7
  <Text>{children}</Text>
@@ -0,0 +1,188 @@
1
+ /* eslint-disable react/display-name */
2
+
3
+ import {getImageDimensions} from '@sanity/asset-utils'
4
+ import {
5
+ ArrayOfObjectsInputProps,
6
+ ImageValue,
7
+ insert,
8
+ ObjectSchemaType,
9
+ PatchEvent,
10
+ set,
11
+ setIfMissing,
12
+ useClient,
13
+ useFormValue,
14
+ } 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
+
21
+ import {IUseResizeObserverCallback, useDebouncedCallback, useResizeObserver} from '@react-hookz/web'
22
+ import Feedback from './Feedback'
23
+ import Spot from './Spot'
24
+ import {ImageHotspotOptions} from './index'
25
+
26
+ const imageStyle = {width: `100%`, height: `auto`}
27
+
28
+ const VALID_ROOT_PATHS = ['document', 'parent']
29
+
30
+ export type FnHotspotMove = (key: string, x: number, y: number) => void
31
+
32
+ export type HotspotItem<HotspotFields = {[key: string]: unknown}> = {
33
+ _key: string
34
+ _type: string
35
+ x: number
36
+ y: number
37
+ } & HotspotFields
38
+
39
+ export function ImageHotspotArray(
40
+ props: ArrayOfObjectsInputProps<HotspotItem> & {imageHotspotOptions: ImageHotspotOptions}
41
+ ) {
42
+ const {value, onChange, imageHotspotOptions, schemaType, renderPreview} = props
43
+
44
+ const sanityClient = useClient({apiVersion: '2022-01-01'})
45
+
46
+ const imageHotspotPathRoot = useMemo(() => {
47
+ const pathRoot = VALID_ROOT_PATHS.includes(imageHotspotOptions.pathRoot ?? '')
48
+ ? imageHotspotOptions.pathRoot
49
+ : 'document'
50
+ return pathRoot === 'document' ? [] : props.path.slice(0, -1)
51
+ }, [imageHotspotOptions.pathRoot, props.path])
52
+
53
+ const rootObject = useFormValue(imageHotspotPathRoot)
54
+
55
+ /**
56
+ * Finding the image from the imageHotspotPathRoot (defaults to document),
57
+ * using the path from the hotspot's `options` field
58
+ *
59
+ * when changes in imageHotspotPathRoot (e.g. document) occur,
60
+ * check if there are any changes to the hotspotImage and update the reference
61
+ */
62
+ const hotspotImage = useMemo(() => {
63
+ return get(rootObject, imageHotspotOptions.imagePath) as ImageValue | undefined
64
+ }, [rootObject, imageHotspotOptions.imagePath])
65
+
66
+ const displayImage = useMemo(() => {
67
+ const builder = imageUrlBuilder(sanityClient).dataset(sanityClient.config().dataset ?? '')
68
+ const urlFor = (source: ImageValue) => builder.image(source)
69
+
70
+ if (hotspotImage?.asset?._ref) {
71
+ const {aspectRatio} = getImageDimensions(hotspotImage.asset._ref)
72
+ const width = 1200
73
+ const height = Math.round(width / aspectRatio)
74
+ const url = urlFor(hotspotImage).width(width).url()
75
+
76
+ return {width, height, url}
77
+ }
78
+
79
+ return null
80
+ }, [hotspotImage, sanityClient])
81
+
82
+ const handleHotspotImageClick = useCallback(
83
+ (event: any) => {
84
+ const {nativeEvent} = event
85
+
86
+ // 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))
89
+ const description = `New Hotspot at ${x}% x ${y}%`
90
+
91
+ const newRow: HotspotItem = {
92
+ _key: randomKey(12),
93
+ _type: schemaType.of[0].name,
94
+ x,
95
+ y,
96
+ }
97
+
98
+ if (imageHotspotOptions?.descriptionPath) {
99
+ newRow[imageHotspotOptions.descriptionPath] = description
100
+ }
101
+
102
+ onChange(PatchEvent.from([setIfMissing([]), insert([newRow], 'after', [-1])]))
103
+ },
104
+ [imageHotspotOptions, onChange, schemaType]
105
+ )
106
+
107
+ const handleHotspotMove: FnHotspotMove = useCallback(
108
+ (key, x, y) => {
109
+ onChange(
110
+ PatchEvent.from([
111
+ // Set the `x` value of this array key item
112
+ set(x, [{_key: key}, 'x']),
113
+ // Set the `y` value of this array key item
114
+ set(y, [{_key: key}, 'y']),
115
+ ])
116
+ )
117
+ },
118
+ [onChange]
119
+ )
120
+
121
+ const hotspotImageRef = useRef<HTMLImageElement | null>(null)
122
+
123
+ const [imageRect, setImageRect] = useState<DOMRectReadOnly>()
124
+ const updateImageRectCallback = useDebouncedCallback(
125
+ ((e) => setImageRect(e.contentRect)) as IUseResizeObserverCallback,
126
+ [setImageRect],
127
+ 200
128
+ )
129
+ useResizeObserver(hotspotImageRef, updateImageRectCallback)
130
+
131
+ return (
132
+ <Stack space={[2, 2, 3]}>
133
+ {displayImage?.url ? (
134
+ <div style={{position: `relative`}}>
135
+ {imageRect &&
136
+ (value?.length ?? 0) > 0 &&
137
+ value?.map((spot, index) => (
138
+ <Spot
139
+ index={index}
140
+ key={spot._key}
141
+ value={spot}
142
+ bounds={imageRect}
143
+ update={handleHotspotMove}
144
+ hotspotDescriptionPath={imageHotspotOptions?.descriptionPath}
145
+ tooltip={imageHotspotOptions?.tooltip}
146
+ renderPreview={renderPreview}
147
+ schemaType={schemaType.of[0] as ObjectSchemaType}
148
+ />
149
+ ))}
150
+
151
+ <Card __unstable_checkered shadow={1}>
152
+ <Flex align="center" justify="center">
153
+ <img
154
+ ref={hotspotImageRef}
155
+ src={displayImage.url}
156
+ width={displayImage.width}
157
+ height={displayImage.height}
158
+ alt=""
159
+ style={imageStyle}
160
+ onClick={handleHotspotImageClick}
161
+ />
162
+ </Flex>
163
+ </Card>
164
+ </div>
165
+ ) : (
166
+ <Feedback>
167
+ {imageHotspotOptions.imagePath ? (
168
+ <>
169
+ No Hotspot image found at path <code>{imageHotspotOptions.imagePath}</code>
170
+ </>
171
+ ) : (
172
+ <>
173
+ Define a path in this field using to the image field in this document at{' '}
174
+ <code>options.hotspotImagePath</code>
175
+ </>
176
+ )}
177
+ </Feedback>
178
+ )}
179
+ {imageHotspotOptions.pathRoot && !VALID_ROOT_PATHS.includes(imageHotspotOptions.pathRoot) && (
180
+ <Feedback>
181
+ The supplied imageHotspotPathRoot "{imageHotspotOptions.pathRoot}" is not valid, falling
182
+ back to "document". Available values are "{VALID_ROOT_PATHS.join(', ')}".
183
+ </Feedback>
184
+ )}
185
+ {props.renderDefault(props as unknown as ArrayOfObjectsInputProps)}
186
+ </Stack>
187
+ )
188
+ }
package/src/Spot.tsx CHANGED
@@ -1,13 +1,16 @@
1
+ /* eslint-disable */
1
2
  import {Box, Text, Tooltip} from '@sanity/ui'
2
3
  import {motion, useMotionValue} from 'framer-motion'
3
4
  import get from 'lodash/get'
4
- import React, {CSSProperties, ReactElement, useEffect} from 'react'
5
- import {FnHotspotMove, TSpot} from './HotspotArray'
5
+ import React, {ComponentType, CSSProperties, ReactElement, useEffect} from 'react'
6
+ import {FnHotspotMove, HotspotItem} from './ImageHotspotArray'
7
+ import {ObjectSchemaType, RenderPreviewCallback} from 'sanity'
6
8
 
7
9
  const dragStyle: CSSProperties = {
8
10
  width: '1.4rem',
9
11
  height: '1.4rem',
10
12
  position: 'absolute',
13
+ boxSizing: 'border-box',
11
14
  top: 0,
12
15
  left: 0,
13
16
  margin: '-0.7rem 0 0 -0.7rem',
@@ -60,39 +63,49 @@ const labelStyleWhileActive: CSSProperties = {
60
63
  visibility: 'hidden',
61
64
  }
62
65
 
63
- const round = (num) => Math.round(num * 100) / 100
66
+ const round = (num: number) => Math.round(num * 100) / 100
64
67
 
65
- interface IHotspot {
66
- spot: TSpot
68
+ export interface HotspotTooltipProps<HotspotFields = {[key: string]: unknown}> {
69
+ value: HotspotItem<HotspotFields>
70
+ schemaType: ObjectSchemaType
71
+ renderPreview: RenderPreviewCallback
72
+ }
73
+
74
+ interface HotspotProps<HotspotFields = {[key: string]: unknown}> {
75
+ value: HotspotItem
67
76
  bounds: DOMRectReadOnly
68
77
  update: FnHotspotMove
69
78
  hotspotDescriptionPath?: string
70
- tooltip?: ReactElement
79
+ tooltip?: ComponentType<HotspotTooltipProps<HotspotFields>>
71
80
  index: number
81
+ schemaType: ObjectSchemaType
82
+ renderPreview: RenderPreviewCallback
72
83
  }
73
84
 
74
85
  export default function Spot({
75
- spot,
86
+ value,
76
87
  bounds,
77
88
  update,
78
89
  hotspotDescriptionPath,
79
90
  tooltip,
80
91
  index,
81
- }: IHotspot) {
92
+ schemaType,
93
+ renderPreview,
94
+ }: HotspotProps) {
82
95
  const [isDragging, setIsDragging] = React.useState(false)
83
96
  const [isHovering, setIsHovering] = React.useState(false)
84
97
 
85
98
  // x/y are stored as % but need to be converted to px
86
- const x = useMotionValue(round(bounds.width * (spot.x / 100)))
87
- const y = useMotionValue(round(bounds.height * (spot.y / 100)))
99
+ const x = useMotionValue(round(bounds.width * (value.x / 100)))
100
+ const y = useMotionValue(round(bounds.height * (value.y / 100)))
88
101
 
89
102
  /**
90
103
  * update x/y if the bounds change when resizing the window
91
104
  */
92
105
  useEffect(() => {
93
- x.set(round(bounds.width * (spot.x / 100)))
94
- y.set(round(bounds.height * (spot.y / 100)))
95
- }, [bounds])
106
+ x.set(round(bounds.width * (value.x / 100)))
107
+ y.set(round(bounds.height * (value.y / 100)))
108
+ }, [x, y, value, bounds])
96
109
 
97
110
  const handleDragEnd = React.useCallback(() => {
98
111
  setIsDragging(false)
@@ -109,8 +122,8 @@ export default function Spot({
109
122
  const safeX = Math.max(0, Math.min(100, newX))
110
123
  const safeY = Math.max(0, Math.min(100, newY))
111
124
 
112
- update(spot._key, safeX, safeY)
113
- }, [spot])
125
+ update(value._key, safeX, safeY)
126
+ }, [x, y, value, update, bounds])
114
127
  const handleDragStart = React.useCallback(() => setIsDragging(true), [])
115
128
 
116
129
  const handleHoverStart = React.useCallback(() => setIsHovering(true), [])
@@ -122,18 +135,18 @@ export default function Spot({
122
135
 
123
136
  return (
124
137
  <Tooltip
125
- key={spot._key}
138
+ key={value._key}
126
139
  disabled={isDragging}
127
140
  portal
128
141
  content={
129
142
  tooltip && typeof tooltip === 'function' ? (
130
- React.createElement(tooltip, {spot})
143
+ React.createElement(tooltip, {value, renderPreview, schemaType})
131
144
  ) : (
132
145
  <Box padding={2} style={{maxWidth: 200, pointerEvents: `none`}}>
133
146
  <Text textOverflow="ellipsis">
134
147
  {hotspotDescriptionPath
135
- ? (get(spot, hotspotDescriptionPath) as string)
136
- : `${spot.x}% x ${spot.y}%`}
148
+ ? (get(value, hotspotDescriptionPath) as string)
149
+ : `${value.x}% x ${value.y}%`}
137
150
  </Text>
138
151
  </Box>
139
152
  )
package/src/index.tsx ADDED
@@ -0,0 +1,42 @@
1
+ import {ImageHotspotArray, type HotspotItem} from './ImageHotspotArray'
2
+ import React, {ComponentType} from 'react'
3
+ import {ArrayOfObjectsInputProps, createPlugin} from 'sanity'
4
+ import {type HotspotTooltipProps} from './Spot'
5
+
6
+ export interface ImageHotspotOptions<HotspotFields = {[key: string]: unknown}> {
7
+ pathRoot?: 'document' | 'parent'
8
+ imagePath: string
9
+ descriptionPath?: string
10
+ tooltip?: ComponentType<HotspotTooltipProps<HotspotFields>>
11
+ }
12
+
13
+ declare module '@sanity/types' {
14
+ export interface ArrayOptions {
15
+ imageHotspot?: ImageHotspotOptions
16
+ }
17
+ }
18
+
19
+ export {ImageHotspotArray}
20
+ export type {HotspotTooltipProps, HotspotItem}
21
+
22
+ export const imageHotspotArrayPlugin = createPlugin({
23
+ name: 'image-hotspot-array',
24
+ form: {
25
+ components: {
26
+ input: (props) => {
27
+ if (props.schemaType.jsonType === 'array' && props.schemaType.options?.imageHotspot) {
28
+ const imageHotspotOptions = props.schemaType.options?.imageHotspot
29
+ if (imageHotspotOptions) {
30
+ return (
31
+ <ImageHotspotArray
32
+ {...(props as unknown as ArrayOfObjectsInputProps<HotspotItem>)}
33
+ imageHotspotOptions={imageHotspotOptions}
34
+ />
35
+ )
36
+ }
37
+ }
38
+ return props.renderDefault(props)
39
+ },
40
+ },
41
+ },
42
+ })
@@ -0,0 +1,11 @@
1
+ const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
2
+ const {name, version, sanityExchangeUrl} = require('./package.json')
3
+
4
+ export default showIncompatiblePluginDialog({
5
+ name: name,
6
+ versions: {
7
+ v3: version,
8
+ v2: undefined,
9
+ },
10
+ sanityExchangeUrl,
11
+ })
package/.babelrc DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "extends": "sanipack/babel"
3
- }
package/lib/Feedback.js DELETED
@@ -1,23 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = Feedback;
7
-
8
- var _react = _interopRequireDefault(require("react"));
9
-
10
- var _ui = require("@sanity/ui");
11
-
12
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
13
-
14
- function Feedback(_ref) {
15
- var children = _ref.children;
16
- return /*#__PURE__*/_react.default.createElement(_ui.Card, {
17
- padding: 4,
18
- radius: 2,
19
- shadow: 1,
20
- tone: "caution"
21
- }, /*#__PURE__*/_react.default.createElement(_ui.Text, null, children));
22
- }
23
- //# sourceMappingURL=Feedback.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Feedback.js","names":["Feedback","children"],"sources":["../src/Feedback.tsx"],"sourcesContent":["import React from 'react'\nimport {Card, Text} from '@sanity/ui'\n\nexport default function Feedback({children}) {\n return (\n <Card padding={4} radius={2} shadow={1} tone=\"caution\">\n <Text>{children}</Text>\n </Card>\n )\n}\n"],"mappings":";;;;;;;AAAA;;AACA;;;;AAEe,SAASA,QAAT,OAA8B;EAAA,IAAXC,QAAW,QAAXA,QAAW;EAC3C,oBACE,6BAAC,QAAD;IAAM,OAAO,EAAE,CAAf;IAAkB,MAAM,EAAE,CAA1B;IAA6B,MAAM,EAAE,CAArC;IAAwC,IAAI,EAAC;EAA7C,gBACE,6BAAC,QAAD,QAAOA,QAAP,CADF,CADF;AAKD"}