sanity-plugin-hotspot-array 0.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/.babelrc +3 -0
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/lib/Feedback.js +23 -0
- package/lib/Feedback.js.map +1 -0
- package/lib/HotspotArray.js +163 -0
- package/lib/HotspotArray.js.map +1 -0
- package/lib/Spot.js +159 -0
- package/lib/Spot.js.map +1 -0
- package/lib/useUnsetInputComponent.js +32 -0
- package/lib/useUnsetInputComponent.js.map +1 -0
- package/package.json +65 -0
- package/sanity.json +7 -0
- package/src/Feedback.tsx +10 -0
- package/src/HotspotArray.tsx +136 -0
- package/src/Spot.tsx +103 -0
- package/src/useUnsetInputComponent.ts +17 -0
package/.babelrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Simeon Griggs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# sanity-plugin-hotspot-array
|
|
2
|
+
|
|
3
|
+
A configurable Custom Input for Arrays that will add and update items by clicking on an Image
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
sanity install hotspot-array
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Import the `HotspotArray` component from this package, into your schema. And insert it as the `inputComponent` of an `array` field.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import HotspotArray from 'sanity-plugin-hotspot-array'
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
name: `product`,
|
|
22
|
+
title: `Product`,
|
|
23
|
+
type: `document`,
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
name: `hotspots`,
|
|
27
|
+
type: `array`,
|
|
28
|
+
inputComponent: HotspotArray,
|
|
29
|
+
of: [
|
|
30
|
+
// see `Spot object` setup below
|
|
31
|
+
],
|
|
32
|
+
options: {
|
|
33
|
+
// see `Image and description path` setup below
|
|
34
|
+
hotspotImagePath: `featureImage`
|
|
35
|
+
hotspotDescriptionPath: `details`
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ...all your other fields
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The plugin makes a number of assumptions to add and update data in the array. Including:
|
|
44
|
+
|
|
45
|
+
- The `array` field is an array of objects
|
|
46
|
+
- The object contains two number fields named `x` and `y`
|
|
47
|
+
- You'll want to save those values as % from the top left of the image
|
|
48
|
+
- The same document contains the image you want to add hotspots to
|
|
49
|
+
|
|
50
|
+
### Image and description paths
|
|
51
|
+
|
|
52
|
+
The custom input has the current values of all fields in the document, and so can "pick" the image out of the document by its path.
|
|
53
|
+
|
|
54
|
+
For example, if you want to add hotspots to an image, and that image is uploaded to the field `featuredImage`, your fields `options` would look like:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
options: {
|
|
58
|
+
hotspotImagePath: `featureImage`
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The custom input can also pre-fill a string or text field with a description of the position of the spot to make them easier to identify. Add a path **relative to the spot object** for this field.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
options: {
|
|
66
|
+
hotspotDescriptionPath: `details`
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Spot object
|
|
71
|
+
|
|
72
|
+
Here's an example object schema complete with initial values, validation, fieldsets and a styled preview.
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
{
|
|
76
|
+
name: 'spot',
|
|
77
|
+
type: 'object',
|
|
78
|
+
fieldsets: [{name: 'position', options: {columns: 2}}],
|
|
79
|
+
fields: [
|
|
80
|
+
{name: 'details', type: 'text', rows: 2},
|
|
81
|
+
{
|
|
82
|
+
name: 'x',
|
|
83
|
+
type: 'number',
|
|
84
|
+
readOnly: true,
|
|
85
|
+
fieldset: 'position',
|
|
86
|
+
initialValue: 50,
|
|
87
|
+
validation: (Rule) => Rule.required().min(0).max(100),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'y',
|
|
91
|
+
type: 'number',
|
|
92
|
+
readOnly: true,
|
|
93
|
+
fieldset: 'position',
|
|
94
|
+
initialValue: 50,
|
|
95
|
+
validation: (Rule) => Rule.required().min(0).max(100),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
preview: {
|
|
99
|
+
select: {
|
|
100
|
+
title: 'description',
|
|
101
|
+
x: 'x',
|
|
102
|
+
y: 'y',
|
|
103
|
+
},
|
|
104
|
+
prepare({title, x, y}) {
|
|
105
|
+
return {
|
|
106
|
+
title,
|
|
107
|
+
subtitle: x && y ? `${x}% x ${y}%` : `No position set`,
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Future cool ideas I just haven't added (yet?)
|
|
115
|
+
|
|
116
|
+
- An `options` key to define a custom preview component for the Spot's `<Tooltip />`
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT © Simeon Griggs
|
|
121
|
+
See LICENSE
|
package/lib/Feedback.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Feedback.tsx"],"names":["Feedback","children"],"mappings":";;;;;;;AAAA;;AACA;;;;AAEe,SAASA,QAAT,OAA8B;AAAA,MAAXC,QAAW,QAAXA,QAAW;AAC3C,sBACE,6BAAC,QAAD;AAAM,IAAA,OAAO,EAAE,CAAf;AAAkB,IAAA,MAAM,EAAE,CAA1B;AAA6B,IAAA,MAAM,EAAE,CAArC;AAAwC,IAAA,IAAI,EAAC;AAA7C,kBACE,6BAAC,QAAD,QAAOA,QAAP,CADF,CADF;AAKD","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"],"file":"Feedback.js"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
|
|
8
|
+
var _client = _interopRequireDefault(require("part:@sanity/base/client"));
|
|
9
|
+
|
|
10
|
+
var _formBuilder = require("part:@sanity/form-builder");
|
|
11
|
+
|
|
12
|
+
var _react = _interopRequireDefault(require("react"));
|
|
13
|
+
|
|
14
|
+
var _ui = require("@sanity/ui");
|
|
15
|
+
|
|
16
|
+
var _FormBuilderInput = require("@sanity/form-builder/lib/FormBuilderInput");
|
|
17
|
+
|
|
18
|
+
var _content = require("@sanity/util/content");
|
|
19
|
+
|
|
20
|
+
var _PatchEvent = require("@sanity/form-builder/PatchEvent");
|
|
21
|
+
|
|
22
|
+
var _imageUrl = _interopRequireDefault(require("@sanity/image-url"));
|
|
23
|
+
|
|
24
|
+
var _assetUtils = require("@sanity/asset-utils");
|
|
25
|
+
|
|
26
|
+
var _get = _interopRequireDefault(require("lodash/get"));
|
|
27
|
+
|
|
28
|
+
var _Spot = _interopRequireDefault(require("./Spot"));
|
|
29
|
+
|
|
30
|
+
var _useUnsetInputComponent = require("./useUnsetInputComponent");
|
|
31
|
+
|
|
32
|
+
var _Feedback = _interopRequireDefault(require("./Feedback"));
|
|
33
|
+
|
|
34
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
35
|
+
|
|
36
|
+
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
37
|
+
|
|
38
|
+
var builder = (0, _imageUrl.default)(_client.default);
|
|
39
|
+
|
|
40
|
+
var urlFor = source => builder.image(source);
|
|
41
|
+
|
|
42
|
+
var imageStyle = {
|
|
43
|
+
width: "100%",
|
|
44
|
+
height: "auto"
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
var HotspotArray = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
|
|
48
|
+
var _type$options4, _type$options5;
|
|
49
|
+
|
|
50
|
+
var type = props.type,
|
|
51
|
+
value = props.value,
|
|
52
|
+
onChange = props.onChange,
|
|
53
|
+
sanityDocument = props.document; // Attempt prevention of infinite loop in <FormBuilderInput />
|
|
54
|
+
// Re-renders can still occur if this Component is used again in a nested field
|
|
55
|
+
|
|
56
|
+
var typeWithoutInputComponent = (0, _useUnsetInputComponent.useUnsetInputComponent)(type, type === null || type === void 0 ? void 0 : type.inputComponent); // Finding the image from the document,
|
|
57
|
+
// using the path from the hotspot's `options` field
|
|
58
|
+
|
|
59
|
+
var displayImage = _react.default.useMemo(() => {
|
|
60
|
+
var _type$options, _hotspotImage$asset;
|
|
61
|
+
|
|
62
|
+
var hotspotImage = (0, _get.default)(sanityDocument, type === null || type === void 0 ? void 0 : (_type$options = type.options) === null || _type$options === void 0 ? void 0 : _type$options.hotspotImagePath);
|
|
63
|
+
|
|
64
|
+
if (hotspotImage !== null && hotspotImage !== void 0 && (_hotspotImage$asset = hotspotImage.asset) !== null && _hotspotImage$asset !== void 0 && _hotspotImage$asset._ref) {
|
|
65
|
+
var _getImageDimensions = (0, _assetUtils.getImageDimensions)(hotspotImage.asset._ref),
|
|
66
|
+
aspectRatio = _getImageDimensions.aspectRatio;
|
|
67
|
+
|
|
68
|
+
var width = 600;
|
|
69
|
+
var height = Math.round(width / aspectRatio);
|
|
70
|
+
var url = urlFor(hotspotImage).width(width).url();
|
|
71
|
+
return {
|
|
72
|
+
width,
|
|
73
|
+
height,
|
|
74
|
+
url
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}, [sanityDocument, type]);
|
|
80
|
+
|
|
81
|
+
var handleHotspotImageClick = _react.default.useCallback(event => {
|
|
82
|
+
var _type$options2;
|
|
83
|
+
|
|
84
|
+
var nativeEvent = event.nativeEvent; // Calculate the x/y percentage of the click position
|
|
85
|
+
|
|
86
|
+
var x = Number((nativeEvent.offsetX * 100 / nativeEvent.srcElement.width).toFixed(2));
|
|
87
|
+
var y = Number((nativeEvent.offsetY * 100 / nativeEvent.srcElement.height).toFixed(2));
|
|
88
|
+
var description = "New Hotspot at ".concat(x, "% x ").concat(y, "%");
|
|
89
|
+
var newRow = {
|
|
90
|
+
_key: (0, _content.randomKey)(12),
|
|
91
|
+
_type: "spot",
|
|
92
|
+
x,
|
|
93
|
+
y
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (type !== null && type !== void 0 && (_type$options2 = type.options) !== null && _type$options2 !== void 0 && _type$options2.hotspotDescriptionPath) {
|
|
97
|
+
console.log(type.options.hotspotDescriptionPath);
|
|
98
|
+
newRow[type.options.hotspotDescriptionPath] = description;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onChange(_PatchEvent.PatchEvent.from((0, _PatchEvent.setIfMissing)([]), (0, _PatchEvent.insert)([newRow], 'after', [-1])));
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
var handleHotspotMove = _react.default.useCallback((key, x, y) => {
|
|
105
|
+
if (!Number(x) || !Number(y)) {
|
|
106
|
+
console.warn("Missing or non-number X or Y", {
|
|
107
|
+
x,
|
|
108
|
+
y
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onChange(_PatchEvent.PatchEvent.from( // Set the `x` value of this array key item
|
|
114
|
+
(0, _PatchEvent.set)(x, [{
|
|
115
|
+
_key: key
|
|
116
|
+
}, 'x']), // Set the `y` value of this array key item
|
|
117
|
+
(0, _PatchEvent.set)(y, [{
|
|
118
|
+
_key: key
|
|
119
|
+
}, 'y'])));
|
|
120
|
+
}, [value]);
|
|
121
|
+
|
|
122
|
+
var hotspotImageRef = _react.default.useRef(null);
|
|
123
|
+
|
|
124
|
+
return /*#__PURE__*/_react.default.createElement(_ui.Stack, {
|
|
125
|
+
space: [2, 2, 3]
|
|
126
|
+
}, displayImage !== null && displayImage !== void 0 && displayImage.url ? /*#__PURE__*/_react.default.createElement("div", {
|
|
127
|
+
style: {
|
|
128
|
+
position: "relative"
|
|
129
|
+
}
|
|
130
|
+
}, (value === null || value === void 0 ? void 0 : value.length) > 0 && value.map(spot => {
|
|
131
|
+
var _type$options3;
|
|
132
|
+
|
|
133
|
+
return /*#__PURE__*/_react.default.createElement(_Spot.default, {
|
|
134
|
+
key: spot._key,
|
|
135
|
+
spot: spot,
|
|
136
|
+
bounds: hotspotImageRef,
|
|
137
|
+
update: handleHotspotMove,
|
|
138
|
+
hotspotDescriptionPath: type === null || type === void 0 ? void 0 : (_type$options3 = type.options) === null || _type$options3 === void 0 ? void 0 : _type$options3.hotspotDescriptionPath
|
|
139
|
+
});
|
|
140
|
+
}), /*#__PURE__*/_react.default.createElement(_ui.Card, {
|
|
141
|
+
__unstable_checkered: true,
|
|
142
|
+
shadow: 1
|
|
143
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Flex, {
|
|
144
|
+
align: "center",
|
|
145
|
+
justify: "center"
|
|
146
|
+
}, /*#__PURE__*/_react.default.createElement("img", {
|
|
147
|
+
ref: hotspotImageRef,
|
|
148
|
+
src: displayImage.url,
|
|
149
|
+
width: displayImage.width,
|
|
150
|
+
height: displayImage.height,
|
|
151
|
+
alt: "",
|
|
152
|
+
style: imageStyle,
|
|
153
|
+
onClick: handleHotspotImageClick
|
|
154
|
+
})))) : /*#__PURE__*/_react.default.createElement(_Feedback.default, null, type !== null && type !== void 0 && (_type$options4 = type.options) !== null && _type$options4 !== void 0 && _type$options4.hotspotImagePath ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, "No Hotspot image found at path ", /*#__PURE__*/_react.default.createElement("code", null, type === null || type === void 0 ? void 0 : (_type$options5 = type.options) === null || _type$options5 === void 0 ? void 0 : _type$options5.hotspotImagePath)) : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, "Define a path in this field using to the image field in this document at ", /*#__PURE__*/_react.default.createElement("code", null, "options.hotspotImagePath"))), /*#__PURE__*/_react.default.createElement(_FormBuilderInput.FormBuilderInput, _extends({}, props, {
|
|
155
|
+
type: typeWithoutInputComponent,
|
|
156
|
+
ref: ref
|
|
157
|
+
})));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
var _default = (0, _formBuilder.withDocument)(HotspotArray);
|
|
161
|
+
|
|
162
|
+
exports.default = _default;
|
|
163
|
+
//# sourceMappingURL=HotspotArray.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/HotspotArray.tsx"],"names":["builder","sanityClient","urlFor","source","image","imageStyle","width","height","HotspotArray","React","forwardRef","props","ref","type","value","onChange","sanityDocument","document","typeWithoutInputComponent","inputComponent","displayImage","useMemo","hotspotImage","options","hotspotImagePath","asset","_ref","aspectRatio","Math","round","url","handleHotspotImageClick","useCallback","event","nativeEvent","x","Number","offsetX","srcElement","toFixed","y","offsetY","description","newRow","_key","_type","hotspotDescriptionPath","console","log","PatchEvent","from","handleHotspotMove","key","warn","hotspotImageRef","useRef","position","length","map","spot"],"mappings":"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","sourcesContent":["/* eslint-disable react/display-name */\n\n// @ts-ignore\nimport sanityClient from 'part:@sanity/base/client'\n// @ts-ignore\nimport {withDocument} from 'part:@sanity/form-builder'\n\nimport React from 'react'\nimport {Card, Flex, Stack, Text} from '@sanity/ui'\nimport {FormBuilderInput} from '@sanity/form-builder/lib/FormBuilderInput'\nimport {randomKey} from '@sanity/util/content'\nimport {PatchEvent, setIfMissing, set, insert} from '@sanity/form-builder/PatchEvent'\nimport imageUrlBuilder from '@sanity/image-url'\nimport {getImageDimensions} from '@sanity/asset-utils'\nimport get from 'lodash/get'\n\nimport Spot from './Spot'\nimport { useUnsetInputComponent } from './useUnsetInputComponent'\nimport Feedback from './Feedback'\n\nconst builder = imageUrlBuilder(sanityClient)\nconst urlFor = (source) => builder.image(source)\nconst imageStyle = {width: `100%`, height: `auto`}\n\n\nconst HotspotArray = React.forwardRef((props, ref) => {\n const {type, value, onChange, document: sanityDocument} = props\n\n // Attempt prevention of infinite loop in <FormBuilderInput />\n // Re-renders can still occur if this Component is used again in a nested field\n const typeWithoutInputComponent = useUnsetInputComponent(type, type?.inputComponent)\n\n // Finding the image from the document,\n // using the path from the hotspot's `options` field\n const displayImage = React.useMemo(() => {\n const hotspotImage = get(sanityDocument, type?.options?.hotspotImagePath)\n\n if (hotspotImage?.asset?._ref) {\n const {aspectRatio} = getImageDimensions(hotspotImage.asset._ref)\n const width = 600\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 }, [sanityDocument, type])\n\n const handleHotspotImageClick = React.useCallback((event) => {\n const {nativeEvent} = event\n\n // Calculate the x/y percentage of the click position\n const x = Number(((nativeEvent.offsetX * 100) / nativeEvent.srcElement.width).toFixed(2))\n const y = Number(((nativeEvent.offsetY * 100) / nativeEvent.srcElement.height).toFixed(2))\n const description = `New Hotspot at ${x}% x ${y}%`\n\n const newRow = {\n _key: randomKey(12),\n _type: `spot`,\n x,\n y,\n }\n\n if (type?.options?.hotspotDescriptionPath) {\n console.log(type.options.hotspotDescriptionPath);\n newRow[type.options.hotspotDescriptionPath] = description\n }\n\n onChange(PatchEvent.from(setIfMissing([]), insert([newRow], 'after', [-1])))\n }, [])\n\n const handleHotspotMove = React.useCallback(\n (key, x, y) => {\n if (!Number(x) || !Number(y)) {\n console.warn(`Missing or non-number X or Y`, {x, y})\n return\n }\n\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 [value]\n )\n\n const hotspotImageRef = React.useRef(null)\n\n return (\n <Stack space={[2,2,3]}>\n {displayImage?.url ? (\n <div style={{position: `relative`}}>\n {value?.length > 0 &&\n value.map((spot) => (\n <Spot\n key={spot._key}\n spot={spot}\n bounds={hotspotImageRef}\n update={handleHotspotMove}\n hotspotDescriptionPath={type?.options?.hotspotDescriptionPath}\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 {type?.options?.hotspotImagePath \n ? <>No Hotspot image found at path <code>{type?.options?.hotspotImagePath}</code></> \n : <>Define a path in this field using to the image field in this document at <code>options.hotspotImagePath</code></>\n }\n </Feedback>\n )}\n <FormBuilderInput {...props} type={typeWithoutInputComponent} ref={ref} />\n </Stack>\n )\n})\n\nexport default withDocument(HotspotArray)\n"],"file":"HotspotArray.js"}
|
package/lib/Spot.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = Spot;
|
|
7
|
+
|
|
8
|
+
var _react = _interopRequireDefault(require("react"));
|
|
9
|
+
|
|
10
|
+
var _ui = require("@sanity/ui");
|
|
11
|
+
|
|
12
|
+
var _framerMotion = require("framer-motion");
|
|
13
|
+
|
|
14
|
+
var _get = _interopRequireDefault(require("lodash/get"));
|
|
15
|
+
|
|
16
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
|
+
|
|
18
|
+
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
|
|
19
|
+
|
|
20
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
21
|
+
|
|
22
|
+
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
23
|
+
|
|
24
|
+
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
25
|
+
|
|
26
|
+
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
|
|
27
|
+
|
|
28
|
+
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
29
|
+
|
|
30
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
31
|
+
|
|
32
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
33
|
+
|
|
34
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
35
|
+
|
|
36
|
+
var dragStyle = {
|
|
37
|
+
width: "1rem",
|
|
38
|
+
height: "1rem",
|
|
39
|
+
position: "absolute"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
var dotStyle = _objectSpread(_objectSpread({}, dragStyle), {}, {
|
|
43
|
+
borderRadius: "50%",
|
|
44
|
+
// make sure pointer events only run on the parent
|
|
45
|
+
pointerEvents: "none"
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
var round = num => Math.round(num * 100) / 100;
|
|
49
|
+
|
|
50
|
+
function Spot(_ref) {
|
|
51
|
+
var spot = _ref.spot,
|
|
52
|
+
_ref$bounds = _ref.bounds,
|
|
53
|
+
bounds = _ref$bounds === void 0 ? undefined : _ref$bounds,
|
|
54
|
+
update = _ref.update,
|
|
55
|
+
_ref$hotspotDescripti = _ref.hotspotDescriptionPath,
|
|
56
|
+
hotspotDescriptionPath = _ref$hotspotDescripti === void 0 ? "" : _ref$hotspotDescripti;
|
|
57
|
+
|
|
58
|
+
// x/y are stored as % but need to be displayed as px
|
|
59
|
+
var _React$useState = _react.default.useState({
|
|
60
|
+
x: 0,
|
|
61
|
+
y: 0
|
|
62
|
+
}),
|
|
63
|
+
_React$useState2 = _slicedToArray(_React$useState, 2),
|
|
64
|
+
_React$useState2$ = _React$useState2[0],
|
|
65
|
+
x = _React$useState2$.x,
|
|
66
|
+
y = _React$useState2$.y,
|
|
67
|
+
setXY = _React$useState2[1];
|
|
68
|
+
|
|
69
|
+
var _React$useState3 = _react.default.useState({
|
|
70
|
+
width: 0,
|
|
71
|
+
height: 0
|
|
72
|
+
}),
|
|
73
|
+
_React$useState4 = _slicedToArray(_React$useState3, 2),
|
|
74
|
+
rect = _React$useState4[0],
|
|
75
|
+
setRect = _React$useState4[1];
|
|
76
|
+
|
|
77
|
+
var _React$useState5 = _react.default.useState(false),
|
|
78
|
+
_React$useState6 = _slicedToArray(_React$useState5, 2),
|
|
79
|
+
isDragging = _React$useState6[0],
|
|
80
|
+
setIsDragging = _React$useState6[1];
|
|
81
|
+
|
|
82
|
+
_react.default.useEffect(() => {
|
|
83
|
+
var _bounds$current;
|
|
84
|
+
|
|
85
|
+
var clientRect = bounds === null || bounds === void 0 ? void 0 : (_bounds$current = bounds.current) === null || _bounds$current === void 0 ? void 0 : _bounds$current.getBoundingClientRect();
|
|
86
|
+
|
|
87
|
+
if (clientRect) {
|
|
88
|
+
// So convert % to px once we know the height/width of the image
|
|
89
|
+
setXY({
|
|
90
|
+
x: round(clientRect.width * (spot.x / 100)),
|
|
91
|
+
y: round(clientRect.height * (spot.y / 100))
|
|
92
|
+
});
|
|
93
|
+
setRect(clientRect);
|
|
94
|
+
}
|
|
95
|
+
}, [bounds, bounds.current]);
|
|
96
|
+
|
|
97
|
+
var handleDragEnd = _react.default.useCallback(event => {
|
|
98
|
+
setIsDragging(false); // I don't know why, but framer-motion doesn't give you the actual transform values
|
|
99
|
+
// So we have to regex the `px` values off the inline styles
|
|
100
|
+
|
|
101
|
+
var _event$srcElement$sty = event.srcElement.style.transform.split(" ").map(v => {
|
|
102
|
+
return v ? v.match(/\(([^)]+)\)/).pop().replace("px", "") : null;
|
|
103
|
+
}),
|
|
104
|
+
_event$srcElement$sty2 = _slicedToArray(_event$srcElement$sty, 2),
|
|
105
|
+
currentX = _event$srcElement$sty2[0],
|
|
106
|
+
currentY = _event$srcElement$sty2[1];
|
|
107
|
+
|
|
108
|
+
if (!Number(currentX) || !Number(currentY)) {
|
|
109
|
+
return console.warn("Missing or non-number X or Y", {
|
|
110
|
+
currentX,
|
|
111
|
+
currentY
|
|
112
|
+
}, event.srcElement);
|
|
113
|
+
} // Which we need to convert back to `%` to patch the document
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
var newX = round(currentX * 100 / rect.width);
|
|
117
|
+
var newY = round(currentY * 100 / rect.height); // Don't go below 0 or above 100
|
|
118
|
+
|
|
119
|
+
var safeX = Math.max(0, Math.min(100, newX));
|
|
120
|
+
var safeY = Math.max(0, Math.min(100, newY));
|
|
121
|
+
update(spot._key, safeX, safeY);
|
|
122
|
+
}, [spot]);
|
|
123
|
+
|
|
124
|
+
if (!x || !y) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return /*#__PURE__*/_react.default.createElement(_ui.Tooltip, {
|
|
129
|
+
key: spot._key,
|
|
130
|
+
disabled: isDragging,
|
|
131
|
+
boundaryElement: bounds.current,
|
|
132
|
+
portal: true,
|
|
133
|
+
content: /*#__PURE__*/_react.default.createElement(_ui.Box, {
|
|
134
|
+
padding: 2,
|
|
135
|
+
style: {
|
|
136
|
+
maxWidth: 200,
|
|
137
|
+
pointerEvents: "none"
|
|
138
|
+
}
|
|
139
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Text, {
|
|
140
|
+
textOverflow: "ellipsis"
|
|
141
|
+
}, hotspotDescriptionPath ? (0, _get.default)(spot, hotspotDescriptionPath) : "".concat(spot.x, "% x ").concat(spot.y, "%")))
|
|
142
|
+
}, /*#__PURE__*/_react.default.createElement(_framerMotion.motion.div, {
|
|
143
|
+
drag: true,
|
|
144
|
+
dragConstraints: bounds,
|
|
145
|
+
dragMomentum: false,
|
|
146
|
+
initial: {
|
|
147
|
+
x,
|
|
148
|
+
y
|
|
149
|
+
},
|
|
150
|
+
onDragEnd: handleDragEnd,
|
|
151
|
+
onDragStart: () => setIsDragging(true),
|
|
152
|
+
style: dragStyle
|
|
153
|
+
}, /*#__PURE__*/_react.default.createElement(_ui.Card, {
|
|
154
|
+
tone: "primary",
|
|
155
|
+
shadow: 3,
|
|
156
|
+
style: dotStyle
|
|
157
|
+
})));
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=Spot.js.map
|
package/lib/Spot.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Spot.tsx"],"names":["dragStyle","width","height","position","dotStyle","borderRadius","pointerEvents","round","num","Math","Spot","spot","bounds","undefined","update","hotspotDescriptionPath","React","useState","x","y","setXY","rect","setRect","isDragging","setIsDragging","useEffect","clientRect","current","getBoundingClientRect","handleDragEnd","useCallback","event","srcElement","style","transform","split","map","v","match","pop","replace","currentX","currentY","Number","console","warn","newX","newY","safeX","max","min","safeY","_key","maxWidth"],"mappings":"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","sourcesContent":["import React from 'react'\nimport {Box, Card, Text, Tooltip} from '@sanity/ui'\nimport {motion} from 'framer-motion'\nimport get from 'lodash/get'\n\nconst dragStyle = {\n width: `1rem`,\n height: `1rem`,\n position: `absolute`,\n}\n\nconst dotStyle = {\n ...dragStyle,\n borderRadius: `50%`,\n // make sure pointer events only run on the parent\n pointerEvents: `none`,\n}\n\nconst round = (num) => Math.round(num * 100) / 100\n\nexport default function Spot({spot, bounds = undefined, update, hotspotDescriptionPath = ``}) {\n // x/y are stored as % but need to be displayed as px\n const [{x, y}, setXY] = React.useState({x: 0, y: 0})\n const [rect, setRect] = React.useState({width: 0, height: 0})\n const [isDragging, setIsDragging] = React.useState(false)\n\n React.useEffect(() => {\n const clientRect = bounds?.current?.getBoundingClientRect()\n\n if (clientRect) {\n // So convert % to px once we know the height/width of the image\n setXY({\n x: round(clientRect.width * (spot.x / 100)),\n y: round(clientRect.height * (spot.y / 100)),\n })\n\n setRect(clientRect)\n }\n }, [bounds, bounds.current])\n\n const handleDragEnd = React.useCallback(\n (event) => {\n setIsDragging(false)\n\n // I don't know why, but framer-motion doesn't give you the actual transform values\n // So we have to regex the `px` values off the inline styles\n const [currentX, currentY] = event.srcElement.style.transform.split(` `).map((v) => {\n return v\n ? v\n .match(/\\(([^)]+)\\)/)\n .pop()\n .replace(`px`, ``)\n : null\n })\n\n if (!Number(currentX) || !Number(currentY)) {\n return console.warn(`Missing or non-number X or Y`, {currentX, currentY}, event.srcElement)\n }\n\n // Which we need to convert back to `%` to patch the document\n const newX = round((currentX * 100) / rect.width)\n const newY = round((currentY * 100) / rect.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(spot._key, safeX, safeY)\n },\n\n [spot]\n )\n\n if (!x || !y) {\n return null\n }\n\n return (\n <Tooltip\n key={spot._key}\n disabled={isDragging}\n boundaryElement={bounds.current}\n portal\n content={\n <Box padding={2} style={{maxWidth: 200, pointerEvents: `none`}}>\n <Text textOverflow=\"ellipsis\">{hotspotDescriptionPath ? get(spot, hotspotDescriptionPath) : `${spot.x}% x ${spot.y}%`}</Text>\n </Box>\n }\n >\n <motion.div\n drag\n dragConstraints={bounds}\n dragMomentum={false}\n initial={{x, y}}\n onDragEnd={handleDragEnd}\n onDragStart={() => setIsDragging(true)}\n style={dragStyle}\n >\n <Card tone=\"primary\" shadow={3} style={dotStyle} />\n </motion.div>\n </Tooltip>\n )\n}"],"file":"Spot.js"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useUnsetInputComponent = useUnsetInputComponent;
|
|
7
|
+
|
|
8
|
+
var _react = _interopRequireDefault(require("react"));
|
|
9
|
+
|
|
10
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
11
|
+
|
|
12
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
13
|
+
|
|
14
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
15
|
+
|
|
16
|
+
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
17
|
+
|
|
18
|
+
function useUnsetInputComponent(type, component) {
|
|
19
|
+
return _react.default.useMemo(() => unsetInputComponent(type, component), [type, component]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function unsetInputComponent(type, component) {
|
|
23
|
+
var t = _objectSpread(_objectSpread({}, type), {}, {
|
|
24
|
+
inputComponent: type.inputComponent === component ? undefined : type.inputComponent
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
var typeOfType = t.type ? unsetInputComponent(t.type, component) : undefined;
|
|
28
|
+
return _objectSpread(_objectSpread({}, t), {}, {
|
|
29
|
+
type: typeOfType
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=useUnsetInputComponent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useUnsetInputComponent.ts"],"names":["useUnsetInputComponent","type","component","React","useMemo","unsetInputComponent","t","inputComponent","undefined","typeOfType"],"mappings":";;;;;;;AAAA;;;;;;;;;;AAEO,SAASA,sBAAT,CAAgCC,IAAhC,EAAsCC,SAAtC,EAAiD;AACpD,SAAOC,eAAMC,OAAN,CAAc,MAAMC,mBAAmB,CAACJ,IAAD,EAAOC,SAAP,CAAvC,EAA0D,CAACD,IAAD,EAAOC,SAAP,CAA1D,CAAP;AACD;;AAED,SAASG,mBAAT,CAA6BJ,IAA7B,EAAmCC,SAAnC,EAA8C;AAC5C,MAAMI,CAAC,mCACFL,IADE;AAELM,IAAAA,cAAc,EAAEN,IAAI,CAACM,cAAL,KAAwBL,SAAxB,GAAoCM,SAApC,GAAgDP,IAAI,CAACM;AAFhE,IAAP;;AAIA,MAAME,UAAU,GAAGH,CAAC,CAACL,IAAF,GAASI,mBAAmB,CAACC,CAAC,CAACL,IAAH,EAASC,SAAT,CAA5B,GAAkDM,SAArE;AACA,yCACKF,CADL;AAEEL,IAAAA,IAAI,EAAEQ;AAFR;AAID","sourcesContent":["import React from 'react'\n\nexport function useUnsetInputComponent(type, component) {\n return React.useMemo(() => unsetInputComponent(type, component), [type, component])\n }\n \n function unsetInputComponent(type, component) {\n const t = {\n ...type,\n inputComponent: type.inputComponent === component ? undefined : type.inputComponent,\n }\n const typeOfType = t.type ? unsetInputComponent(t.type, component) : undefined\n return {\n ...t,\n type: typeOfType,\n }\n }"],"file":"useUnsetInputComponent.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-plugin-hotspot-array",
|
|
3
|
+
"version": "0.0.1",
|
|
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
|
+
"lint": "eslint .",
|
|
8
|
+
"build": "sanipack build",
|
|
9
|
+
"watch": "sanipack build --watch",
|
|
10
|
+
"prepublishOnly": "sanipack build && sanipack verify"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+ssh://git@github.com/SimeonGriggs/sanity-plugin-hotspot-array.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sanity",
|
|
18
|
+
"sanity-plugin"
|
|
19
|
+
],
|
|
20
|
+
"author": "Simeon Griggs <simeon@sanity.io>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@sanity/asset-utils": "^1.2.3",
|
|
24
|
+
"@sanity/base": "^2.23.2",
|
|
25
|
+
"@sanity/form-builder": "^2.23.2",
|
|
26
|
+
"@sanity/ui": "^0.36.17",
|
|
27
|
+
"framer-motion": "^5.5.5",
|
|
28
|
+
"prop-types": "^15.7.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"eslint": "^7.32.0",
|
|
32
|
+
"eslint-config-prettier": "^8.3.0",
|
|
33
|
+
"eslint-config-sanity": "^5.1.0",
|
|
34
|
+
"eslint-plugin-react": "^7.27.1",
|
|
35
|
+
"prettier": "^2.5.1",
|
|
36
|
+
"sanipack": "^2.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": "^17.0.0",
|
|
40
|
+
"@sanity/util": "2.23.2",
|
|
41
|
+
"@sanity/image-url": "1.0.1",
|
|
42
|
+
"lodash": "4.17.21"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/SimeonGriggs/sanity-plugin-hotspot-array/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/SimeonGriggs/sanity-plugin-hotspot-array#readme",
|
|
48
|
+
"prettier": {
|
|
49
|
+
"semi": false,
|
|
50
|
+
"printWidth": 100,
|
|
51
|
+
"bracketSpacing": false,
|
|
52
|
+
"singleQuote": true
|
|
53
|
+
},
|
|
54
|
+
"eslintConfig": {
|
|
55
|
+
"parser": "sanipack/babel/eslint-parser",
|
|
56
|
+
"extends": [
|
|
57
|
+
"sanity",
|
|
58
|
+
"sanity/react",
|
|
59
|
+
"prettier"
|
|
60
|
+
],
|
|
61
|
+
"ignorePatterns": [
|
|
62
|
+
"lib/**/"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
package/sanity.json
ADDED
package/src/Feedback.tsx
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* eslint-disable react/display-name */
|
|
2
|
+
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import sanityClient from 'part:@sanity/base/client'
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import {withDocument} from 'part:@sanity/form-builder'
|
|
7
|
+
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import {Card, Flex, Stack, Text} from '@sanity/ui'
|
|
10
|
+
import {FormBuilderInput} from '@sanity/form-builder/lib/FormBuilderInput'
|
|
11
|
+
import {randomKey} from '@sanity/util/content'
|
|
12
|
+
import {PatchEvent, setIfMissing, set, insert} from '@sanity/form-builder/PatchEvent'
|
|
13
|
+
import imageUrlBuilder from '@sanity/image-url'
|
|
14
|
+
import {getImageDimensions} from '@sanity/asset-utils'
|
|
15
|
+
import get from 'lodash/get'
|
|
16
|
+
|
|
17
|
+
import Spot from './Spot'
|
|
18
|
+
import { useUnsetInputComponent } from './useUnsetInputComponent'
|
|
19
|
+
import Feedback from './Feedback'
|
|
20
|
+
|
|
21
|
+
const builder = imageUrlBuilder(sanityClient)
|
|
22
|
+
const urlFor = (source) => builder.image(source)
|
|
23
|
+
const imageStyle = {width: `100%`, height: `auto`}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const HotspotArray = React.forwardRef((props, ref) => {
|
|
27
|
+
const {type, value, onChange, document: sanityDocument} = props
|
|
28
|
+
|
|
29
|
+
// Attempt prevention of infinite loop in <FormBuilderInput />
|
|
30
|
+
// Re-renders can still occur if this Component is used again in a nested field
|
|
31
|
+
const typeWithoutInputComponent = useUnsetInputComponent(type, type?.inputComponent)
|
|
32
|
+
|
|
33
|
+
// Finding the image from the document,
|
|
34
|
+
// using the path from the hotspot's `options` field
|
|
35
|
+
const displayImage = React.useMemo(() => {
|
|
36
|
+
const hotspotImage = get(sanityDocument, type?.options?.hotspotImagePath)
|
|
37
|
+
|
|
38
|
+
if (hotspotImage?.asset?._ref) {
|
|
39
|
+
const {aspectRatio} = getImageDimensions(hotspotImage.asset._ref)
|
|
40
|
+
const width = 600
|
|
41
|
+
const height = Math.round(width / aspectRatio)
|
|
42
|
+
const url = urlFor(hotspotImage).width(width).url()
|
|
43
|
+
|
|
44
|
+
return {width, height, url}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null
|
|
48
|
+
}, [sanityDocument, type])
|
|
49
|
+
|
|
50
|
+
const handleHotspotImageClick = React.useCallback((event) => {
|
|
51
|
+
const {nativeEvent} = event
|
|
52
|
+
|
|
53
|
+
// Calculate the x/y percentage of the click position
|
|
54
|
+
const x = Number(((nativeEvent.offsetX * 100) / nativeEvent.srcElement.width).toFixed(2))
|
|
55
|
+
const y = Number(((nativeEvent.offsetY * 100) / nativeEvent.srcElement.height).toFixed(2))
|
|
56
|
+
const description = `New Hotspot at ${x}% x ${y}%`
|
|
57
|
+
|
|
58
|
+
const newRow = {
|
|
59
|
+
_key: randomKey(12),
|
|
60
|
+
_type: `spot`,
|
|
61
|
+
x,
|
|
62
|
+
y,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (type?.options?.hotspotDescriptionPath) {
|
|
66
|
+
console.log(type.options.hotspotDescriptionPath);
|
|
67
|
+
newRow[type.options.hotspotDescriptionPath] = description
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onChange(PatchEvent.from(setIfMissing([]), insert([newRow], 'after', [-1])))
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
const handleHotspotMove = React.useCallback(
|
|
74
|
+
(key, x, y) => {
|
|
75
|
+
if (!Number(x) || !Number(y)) {
|
|
76
|
+
console.warn(`Missing or non-number X or Y`, {x, y})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
onChange(
|
|
81
|
+
PatchEvent.from(
|
|
82
|
+
// Set the `x` value of this array key item
|
|
83
|
+
set(x, [{_key: key}, 'x']),
|
|
84
|
+
// Set the `y` value of this array key item
|
|
85
|
+
set(y, [{_key: key}, 'y'])
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
[value]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const hotspotImageRef = React.useRef(null)
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Stack space={[2,2,3]}>
|
|
96
|
+
{displayImage?.url ? (
|
|
97
|
+
<div style={{position: `relative`}}>
|
|
98
|
+
{value?.length > 0 &&
|
|
99
|
+
value.map((spot) => (
|
|
100
|
+
<Spot
|
|
101
|
+
key={spot._key}
|
|
102
|
+
spot={spot}
|
|
103
|
+
bounds={hotspotImageRef}
|
|
104
|
+
update={handleHotspotMove}
|
|
105
|
+
hotspotDescriptionPath={type?.options?.hotspotDescriptionPath}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
|
|
109
|
+
<Card __unstable_checkered shadow={1}>
|
|
110
|
+
<Flex align="center" justify="center">
|
|
111
|
+
<img
|
|
112
|
+
ref={hotspotImageRef}
|
|
113
|
+
src={displayImage.url}
|
|
114
|
+
width={displayImage.width}
|
|
115
|
+
height={displayImage.height}
|
|
116
|
+
alt=""
|
|
117
|
+
style={imageStyle}
|
|
118
|
+
onClick={handleHotspotImageClick}
|
|
119
|
+
/>
|
|
120
|
+
</Flex>
|
|
121
|
+
</Card>
|
|
122
|
+
</div>
|
|
123
|
+
) : (
|
|
124
|
+
<Feedback>
|
|
125
|
+
{type?.options?.hotspotImagePath
|
|
126
|
+
? <>No Hotspot image found at path <code>{type?.options?.hotspotImagePath}</code></>
|
|
127
|
+
: <>Define a path in this field using to the image field in this document at <code>options.hotspotImagePath</code></>
|
|
128
|
+
}
|
|
129
|
+
</Feedback>
|
|
130
|
+
)}
|
|
131
|
+
<FormBuilderInput {...props} type={typeWithoutInputComponent} ref={ref} />
|
|
132
|
+
</Stack>
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
export default withDocument(HotspotArray)
|
package/src/Spot.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Box, Card, Text, Tooltip} from '@sanity/ui'
|
|
3
|
+
import {motion} from 'framer-motion'
|
|
4
|
+
import get from 'lodash/get'
|
|
5
|
+
|
|
6
|
+
const dragStyle = {
|
|
7
|
+
width: `1rem`,
|
|
8
|
+
height: `1rem`,
|
|
9
|
+
position: `absolute`,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const dotStyle = {
|
|
13
|
+
...dragStyle,
|
|
14
|
+
borderRadius: `50%`,
|
|
15
|
+
// make sure pointer events only run on the parent
|
|
16
|
+
pointerEvents: `none`,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const round = (num) => Math.round(num * 100) / 100
|
|
20
|
+
|
|
21
|
+
export default function Spot({spot, bounds = undefined, update, hotspotDescriptionPath = ``}) {
|
|
22
|
+
// x/y are stored as % but need to be displayed as px
|
|
23
|
+
const [{x, y}, setXY] = React.useState({x: 0, y: 0})
|
|
24
|
+
const [rect, setRect] = React.useState({width: 0, height: 0})
|
|
25
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
const clientRect = bounds?.current?.getBoundingClientRect()
|
|
29
|
+
|
|
30
|
+
if (clientRect) {
|
|
31
|
+
// So convert % to px once we know the height/width of the image
|
|
32
|
+
setXY({
|
|
33
|
+
x: round(clientRect.width * (spot.x / 100)),
|
|
34
|
+
y: round(clientRect.height * (spot.y / 100)),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
setRect(clientRect)
|
|
38
|
+
}
|
|
39
|
+
}, [bounds, bounds.current])
|
|
40
|
+
|
|
41
|
+
const handleDragEnd = React.useCallback(
|
|
42
|
+
(event) => {
|
|
43
|
+
setIsDragging(false)
|
|
44
|
+
|
|
45
|
+
// I don't know why, but framer-motion doesn't give you the actual transform values
|
|
46
|
+
// So we have to regex the `px` values off the inline styles
|
|
47
|
+
const [currentX, currentY] = event.srcElement.style.transform.split(` `).map((v) => {
|
|
48
|
+
return v
|
|
49
|
+
? v
|
|
50
|
+
.match(/\(([^)]+)\)/)
|
|
51
|
+
.pop()
|
|
52
|
+
.replace(`px`, ``)
|
|
53
|
+
: null
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (!Number(currentX) || !Number(currentY)) {
|
|
57
|
+
return console.warn(`Missing or non-number X or Y`, {currentX, currentY}, event.srcElement)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Which we need to convert back to `%` to patch the document
|
|
61
|
+
const newX = round((currentX * 100) / rect.width)
|
|
62
|
+
const newY = round((currentY * 100) / rect.height)
|
|
63
|
+
|
|
64
|
+
// Don't go below 0 or above 100
|
|
65
|
+
const safeX = Math.max(0, Math.min(100, newX))
|
|
66
|
+
const safeY = Math.max(0, Math.min(100, newY))
|
|
67
|
+
|
|
68
|
+
update(spot._key, safeX, safeY)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
[spot]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if (!x || !y) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Tooltip
|
|
80
|
+
key={spot._key}
|
|
81
|
+
disabled={isDragging}
|
|
82
|
+
boundaryElement={bounds.current}
|
|
83
|
+
portal
|
|
84
|
+
content={
|
|
85
|
+
<Box padding={2} style={{maxWidth: 200, pointerEvents: `none`}}>
|
|
86
|
+
<Text textOverflow="ellipsis">{hotspotDescriptionPath ? get(spot, hotspotDescriptionPath) : `${spot.x}% x ${spot.y}%`}</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
<motion.div
|
|
91
|
+
drag
|
|
92
|
+
dragConstraints={bounds}
|
|
93
|
+
dragMomentum={false}
|
|
94
|
+
initial={{x, y}}
|
|
95
|
+
onDragEnd={handleDragEnd}
|
|
96
|
+
onDragStart={() => setIsDragging(true)}
|
|
97
|
+
style={dragStyle}
|
|
98
|
+
>
|
|
99
|
+
<Card tone="primary" shadow={3} style={dotStyle} />
|
|
100
|
+
</motion.div>
|
|
101
|
+
</Tooltip>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export function useUnsetInputComponent(type, component) {
|
|
4
|
+
return React.useMemo(() => unsetInputComponent(type, component), [type, component])
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function unsetInputComponent(type, component) {
|
|
8
|
+
const t = {
|
|
9
|
+
...type,
|
|
10
|
+
inputComponent: type.inputComponent === component ? undefined : type.inputComponent,
|
|
11
|
+
}
|
|
12
|
+
const typeOfType = t.type ? unsetInputComponent(t.type, component) : undefined
|
|
13
|
+
return {
|
|
14
|
+
...t,
|
|
15
|
+
type: typeOfType,
|
|
16
|
+
}
|
|
17
|
+
}
|