carbon-addons-iot-react 5.8.1 → 5.8.2

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.
@@ -315,6 +315,7 @@ var HotspotEditorModal = function HotspotEditorModal(_ref) {
315
315
  switchCurrentType = _useHotspotEditorStat.switchCurrentType,
316
316
  updateHotspotDataSource = _useHotspotEditorStat.updateHotspotDataSource,
317
317
  updateHotspotTooltip = _useHotspotEditorStat.updateHotspotTooltip,
318
+ updateHotspotPosition = _useHotspotEditorStat.updateHotspotPosition,
318
319
  updateTextHotspotStyle = _useHotspotEditorStat.updateTextHotspotStyle,
319
320
  updateTextHotspotContent = _useHotspotEditorStat.updateTextHotspotContent,
320
321
  updateDynamicHotspotSourceX = _useHotspotEditorStat.updateDynamicHotspotSourceX,
@@ -380,6 +381,8 @@ var HotspotEditorModal = function HotspotEditorModal(_ref) {
380
381
  }
381
382
  var hotspotsWithoutExampleValues = filteredHotspots.map(function (hotspot) {
382
383
  return update(hotspot, {
384
+ $unset: ['id'],
385
+ // Remove the internal id added for drag tracking
383
386
  content: {
384
387
  $unset: ['values']
385
388
  }
@@ -629,6 +632,7 @@ var HotspotEditorModal = function HotspotEditorModal(_ref) {
629
632
  hotspotDefaults: hotspotDefaults
630
633
  });
631
634
  },
635
+ onUpdateHotspotPosition: updateHotspotPosition,
632
636
  onSelectHotspot: setSelectedHotspot,
633
637
  selectedHotspots: getSelectedHotspotsList(selectedHotspot, hotspots),
634
638
  src: cardConfig.content.src,
@@ -49,6 +49,7 @@ var hotspotActionTypes = {
49
49
  hotspotDataSourceChange: 'HOTSPOT_DATA_SOURCE_CHANGE',
50
50
  hotspotDataSourceSettingsChange: 'HOTSPOT_DATA_SOURCE_SETTINGS_CHANGE',
51
51
  hotspotTooltipChange: 'HOTSPOT_TOOLTIP_CHANGE',
52
+ hotspotPositionChange: 'HOTSPOT_POSITION_CHANGE',
52
53
  hotspotSelect: 'HOTSPOT_SELECT',
53
54
  hotspotsAdd: 'HOTSPOTS_ADD',
54
55
  textHotspotStyleChange: 'TEXT_HOTSPOT_STYLE_CHANGE',
@@ -151,10 +152,32 @@ function hotspotEditorReducer(state, _ref2) {
151
152
  };
152
153
  return getHotspotUpdate(state, _mergeSpec);
153
154
  }
155
+ // HOTSPOT POSITION CHANGE
156
+ case hotspotActionTypes.hotspotPositionChange:
157
+ {
158
+ var isPositionAvailable = !state.hotspots.find(function (hotspot) {
159
+ return isHotspotMatch(hotspot, payload.position);
160
+ });
161
+ if (isPositionAvailable) {
162
+ // Find the updated hotspot in the new hotspots array to maintain selection
163
+ var updatedSelectedHotspot = payload.newHotspots.find(function (hotspot) {
164
+ return isHotspotMatch(hotspot, payload.position);
165
+ });
166
+ return update(state, {
167
+ hotspots: {
168
+ $set: payload.newHotspots
169
+ },
170
+ selectedHotspot: {
171
+ $set: updatedSelectedHotspot
172
+ }
173
+ });
174
+ }
175
+ return state;
176
+ }
154
177
  // HOTSPOTS ADD
155
178
  case hotspotActionTypes.hotspotsAdd:
156
179
  {
157
- var isPositionAvailable = !state.hotspots.find(function (hotspot) {
180
+ var _isPositionAvailable = !state.hotspots.find(function (hotspot) {
158
181
  return isHotspotMatch(hotspot, payload.position);
159
182
  });
160
183
  var defaultContent = state.currentType === hotspotTypes.TEXT ? {
@@ -165,7 +188,7 @@ function hotspotEditorReducer(state, _ref2) {
165
188
  content: defaultContent,
166
189
  type: createableType
167
190
  });
168
- return isPositionAvailable ? update(state, {
191
+ return _isPositionAvailable ? update(state, {
169
192
  selectedHotspot: {
170
193
  $set: newHotspot
171
194
  },
@@ -193,7 +216,7 @@ function hotspotEditorReducer(state, _ref2) {
193
216
  $set: hotspot
194
217
  },
195
218
  currentType: {
196
- $set: (_hotspot$type = hotspot.type) !== null && _hotspot$type !== void 0 ? _hotspot$type : defaultTypeWhenMissing
219
+ $set: (_hotspot$type = hotspot === null || hotspot === void 0 ? void 0 : hotspot.type) !== null && _hotspot$type !== void 0 ? _hotspot$type : defaultTypeWhenMissing
197
220
  }
198
221
  });
199
222
  }
@@ -392,6 +415,12 @@ function useHotspotEditorState() {
392
415
  payload: hotspotContent
393
416
  });
394
417
  };
418
+ var updateHotspotPosition = function updateHotspotPosition(hotspotPosition) {
419
+ return dispatch({
420
+ type: hotspotActionTypes.hotspotPositionChange,
421
+ payload: hotspotPosition
422
+ });
423
+ };
395
424
 
396
425
  /** Updates the properties of the text hotspot, passes a payload like {color: 'blue'} */
397
426
  var updateTextHotspotStyle = function updateTextHotspotStyle(textHotspotStyle) {
@@ -479,6 +508,7 @@ function useHotspotEditorState() {
479
508
  switchCurrentType: switchCurrentType,
480
509
  updateHotspotDataSource: updateHotspotDataSource,
481
510
  updateHotspotTooltip: updateHotspotTooltip,
511
+ updateHotspotPosition: updateHotspotPosition,
482
512
  updateTextHotspotStyle: updateTextHotspotStyle,
483
513
  updateTextHotspotContent: updateTextHotspotContent,
484
514
  updateDynamicHotspotSourceX: updateDynamicHotspotSourceX,
@@ -174,7 +174,7 @@ var HotspotContent = function HotspotContent(_ref) {
174
174
  }, typeof value === 'number' ? formatNumberWithPrecision(value, !isNil(precision) ? precision : Math.abs(value) < 1 ? value === 0 ? 0 : 3 // for small decimals give 3 spots
175
175
  : 1,
176
176
  // otherwise 1 spot if precision isn't set
177
- locale) : value, unit && value !== '--' && /*#__PURE__*/React__default.createElement("span", {
177
+ locale) : typeof value === 'boolean' ? String(value) : value, unit && value !== '--' && /*#__PURE__*/React__default.createElement("span", {
178
178
  className: "".concat(iotPrefix, "--hotspot-content-unit")
179
179
  }, unit))));
180
180
  }));
@@ -16,7 +16,7 @@ import 'core-js/modules/es.object.keys.js';
16
16
  import 'core-js/modules/es.object.to-string.js';
17
17
  import 'core-js/modules/web.dom-collections.for-each.js';
18
18
  import 'core-js/modules/web.dom-collections.iterator.js';
19
- import React__default, { useState, useMemo, useCallback, useEffect } from 'react';
19
+ import React__default, { useState, useRef, useMemo, useEffect, useCallback } from 'react';
20
20
  import PropTypes from 'prop-types';
21
21
  import { InlineLoading } from '@carbon/react';
22
22
  import warning from 'warning';
@@ -75,6 +75,10 @@ var propTypes = {
75
75
  * Emits position obj {x, y} of hotspot to be added.
76
76
  */
77
77
  onAddHotspotPosition: PropTypes.func,
78
+ /** Callback when a hotspot is dragged to new position in isEditable mode
79
+ * Emits new hotspots and updated position obj {x, y} of hotspot.
80
+ */
81
+ onUpdateHotspotPosition: PropTypes.func,
78
82
  /** Callback when a hotspot is clicked in isEditable mode, emits position obj {x, y} */
79
83
  onSelectHotspot: PropTypes.func,
80
84
  /**
@@ -115,6 +119,7 @@ var defaultProps = {
115
119
  isHotspotDataLoading: false,
116
120
  isEditable: false,
117
121
  onAddHotspotPosition: function onAddHotspotPosition() {},
122
+ onUpdateHotspotPosition: function onUpdateHotspotPosition() {},
118
123
  onSelectHotspot: function onSelectHotspot() {},
119
124
  onHotspotContentChanged: function onHotspotContentChanged() {},
120
125
  background: '#eee',
@@ -233,6 +238,7 @@ var calculateHotspotContainerLayout = function calculateHotspotContainerLayout(_
233
238
  var width;
234
239
  var height;
235
240
  var top;
241
+ var left;
236
242
 
237
243
  // CONTAIN
238
244
  if (objectFit === 'contain') {
@@ -240,32 +246,38 @@ var calculateHotspotContainerLayout = function calculateHotspotContainerLayout(_
240
246
  width = imageWidth;
241
247
  height = imageWidth / imageRatio;
242
248
  top = imageScale > 1 ? imageOffsetY : imageObjectFitOffsetY;
249
+ left = imageScale > 1 ? 0 : (containerWidth - imageWidth) / 2;
243
250
  } else if (imageOrientation === 'portrait') {
244
251
  width = imageHeight / imageRatio;
245
252
  height = imageHeight;
246
253
  top = imageOffsetY;
254
+ left = (containerWidth - width) / 2;
247
255
  }
248
256
  // FILL
249
257
  } else if (objectFit === 'fill') {
250
258
  width = imageScale > 1 ? imageWidth : containerWidth;
251
259
  height = imageScale > 1 ? imageHeight : containerHeight;
252
260
  top = imageOffsetY;
261
+ left = 0;
253
262
  // NO OBJECT FIT
254
263
  } else if (!objectFit) {
255
264
  if (imageOrientation === 'landscape') {
256
265
  width = imageWidth;
257
266
  height = imageWidth / imageRatio;
258
267
  top = imageOffsetY;
268
+ left = 0;
259
269
  } else if (imageOrientation === 'portrait') {
260
270
  width = imageHeight / imageRatio;
261
271
  height = imageHeight;
262
272
  top = imageOffsetY;
273
+ left = (containerWidth - width) / 2;
263
274
  }
264
275
  }
265
276
  return {
266
277
  width: width,
267
278
  height: height,
268
- top: top
279
+ top: top,
280
+ left: left
269
281
  };
270
282
  };
271
283
  var calculateObjectFitOffset = function calculateObjectFitOffset(_ref5) {
@@ -510,6 +522,7 @@ var ImageHotspots = function ImageHotspots(_ref9) {
510
522
  isEditable = _ref9.isEditable,
511
523
  isHotspotDataLoading = _ref9.isHotspotDataLoading,
512
524
  onAddHotspotPosition = _ref9.onAddHotspotPosition,
525
+ onUpdateHotspotPosition = _ref9.onUpdateHotspotPosition,
513
526
  onSelectHotspot = _ref9.onSelectHotspot,
514
527
  onHotspotContentChanged = _ref9.onHotspotContentChanged,
515
528
  zoomMax = _ref9.zoomMax,
@@ -554,9 +567,52 @@ var ImageHotspots = function ImageHotspots(_ref9) {
554
567
  _useState12 = _slicedToArray(_useState11, 2),
555
568
  options = _useState12[0],
556
569
  setOptions = _useState12[1];
570
+ // Tracks if a hotspot is being dragged and which one.
571
+ var _useState13 = useState(null),
572
+ _useState14 = _slicedToArray(_useState13, 2),
573
+ draggingHotspotId = _useState14[0],
574
+ setDraggingHotspotId = _useState14[1];
575
+
576
+ // Ref for outer container
577
+ var containerRef = useRef(null);
578
+ var dragStateRef = useRef({
579
+ // Flag to indicate if a drag is active
580
+ isDragging: false,
581
+ // Stores the starting mouse position of a drag
582
+ dragStartPosition: {
583
+ x: 0,
584
+ y: 0
585
+ },
586
+ // Tracks the current position during a drag
587
+ currentDargPosition: null,
588
+ // Stores layout of image and container
589
+ layout: {
590
+ rect: null,
591
+ hotspotLayout: null
592
+ },
593
+ // Tracks Scheduled Animation Frames
594
+ frame: null
595
+ });
596
+
597
+ // Minimum pixel movement before a drag is initiated
598
+ var DRAG_THRESHOLD = 5;
557
599
  var mergedI18n = useMemo(function () {
558
600
  return _objectSpread(_objectSpread({}, defaultProps.i18n), i18n);
559
601
  }, [i18n]);
602
+ var hotspotsWithId = useMemo(function () {
603
+ return hotspots.map(function (hotspot, index) {
604
+ return _objectSpread(_objectSpread({}, hotspot), {}, {
605
+ id: "".concat(hotspot.x, "-").concat(hotspot.y, "-").concat(index)
606
+ });
607
+ });
608
+ }, [hotspots]);
609
+ var _useState15 = useState(hotspotsWithId),
610
+ _useState16 = _slicedToArray(_useState15, 2),
611
+ editableHotspots = _useState16[0],
612
+ setEditableHotspots = _useState16[1];
613
+ useEffect(function () {
614
+ setEditableHotspots(hotspotsWithId);
615
+ }, [hotspotsWithId]);
560
616
  var handleCtrlKeyUp = useCallback(function (event) {
561
617
  // Was the control key unpressed
562
618
  if (event.key === keyboardKeys.CONTROL) {
@@ -628,6 +684,11 @@ var ImageHotspots = function ImageHotspots(_ref9) {
628
684
  objectFit: displayOption
629
685
  };
630
686
  var onHotspotClicked = useCallback(function (evt, position) {
687
+ // prevent click behavior after drag
688
+ if (dragStateRef.current.isDragging) {
689
+ return;
690
+ }
691
+
631
692
  // It is possible to receive two events here, one Mouse event and one Pointer event.
632
693
  // When used in the ImageHotspots component the Pointer event can somehow be from a
633
694
  // previously clicked hotspot. See https://github.com/carbon-design-system/carbon-addons-iot-react/issues/1803
@@ -636,6 +697,108 @@ var ImageHotspots = function ImageHotspots(_ref9) {
636
697
  onSelectHotspot(position);
637
698
  }
638
699
  }, [onSelectHotspot, isEditable]);
700
+ var handleMouseDownHotspot = useCallback(function (e, id1) {
701
+ var hotspot = editableHotspots.find(function (h) {
702
+ return h.id === id1;
703
+ });
704
+ if (!isEditable || (hotspot === null || hotspot === void 0 ? void 0 : hotspot.type) === 'dynamic') {
705
+ return;
706
+ }
707
+ e.stopPropagation();
708
+ setDraggingHotspotId(id1);
709
+ dragStateRef.current.isDragging = true;
710
+ dragStateRef.current.dragStartPosition = {
711
+ x: e.clientX,
712
+ y: e.clientY
713
+ };
714
+
715
+ // Calculate layout once at drag start
716
+ if (containerRef.current) {
717
+ var rect = containerRef.current.getBoundingClientRect();
718
+ var hotspotLayout = calculateHotspotContainerLayout(image, container, displayOption);
719
+ dragStateRef.current.layout = {
720
+ rect: rect,
721
+ hotspotLayout: hotspotLayout
722
+ };
723
+ }
724
+ }, [container, displayOption, image, isEditable, editableHotspots]);
725
+ var handleMouseMoveHotspot = useCallback(function (e) {
726
+ if (draggingHotspotId !== null && containerRef.current) {
727
+ var _dragStateRef$current = dragStateRef.current,
728
+ isDragging = _dragStateRef$current.isDragging,
729
+ dragStartPosition = _dragStateRef$current.dragStartPosition,
730
+ layout = _dragStateRef$current.layout;
731
+ var dx = e.clientX - dragStartPosition.x;
732
+ var dy = e.clientY - dragStartPosition.y;
733
+ var distance = Math.sqrt(dx * dx + dy * dy);
734
+ if (distance > DRAG_THRESHOLD && isDragging) {
735
+ var rect = layout.rect,
736
+ hotspotLayout = layout.hotspotLayout;
737
+ var x = (e.clientX - rect.left - hotspotLayout.left) / hotspotLayout.width * 100;
738
+ var y = (e.clientY - rect.top - hotspotLayout.top) / hotspotLayout.height * 100;
739
+ // Clamp within image boundaries
740
+ x = Math.max(0, Math.min(100, x));
741
+ y = Math.max(0, Math.min(100, y));
742
+ dragStateRef.current.currentDargPosition = {
743
+ x: x,
744
+ y: y
745
+ };
746
+
747
+ // throttle with requestAnimationFrame
748
+ if (dragStateRef.current.frame === null) {
749
+ dragStateRef.current.frame = requestAnimationFrame(function () {
750
+ setEditableHotspots(function (prev) {
751
+ return prev.map(function (item) {
752
+ return item.id === draggingHotspotId ? _objectSpread(_objectSpread({}, item), {}, {
753
+ x: dragStateRef.current.currentDargPosition.x,
754
+ y: dragStateRef.current.currentDargPosition.y
755
+ }) : item;
756
+ });
757
+ });
758
+ dragStateRef.current.frame = null;
759
+ });
760
+ }
761
+ }
762
+ }
763
+ }, [draggingHotspotId]);
764
+ var handleMouseUpHotspot = useCallback(function (e) {
765
+ if (draggingHotspotId !== null && dragStateRef.current.isDragging) {
766
+ e.stopPropagation();
767
+ var _ref10 = dragStateRef.current.currentDargPosition || {},
768
+ x = _ref10.x,
769
+ y = _ref10.y;
770
+ onUpdateHotspotPosition({
771
+ newHotspots: editableHotspots,
772
+ position: {
773
+ x: x,
774
+ y: y
775
+ }
776
+ });
777
+ setDraggingHotspotId(null);
778
+ dragStateRef.current.currentDargPosition = null;
779
+ dragStateRef.current.isDragging = false;
780
+ }
781
+ }, [editableHotspots, draggingHotspotId, onUpdateHotspotPosition]);
782
+ useEffect(function () {
783
+ var dragState = dragStateRef.current;
784
+ return function () {
785
+ if (dragState.frame !== null) {
786
+ cancelAnimationFrame(dragState.frame);
787
+ }
788
+ };
789
+ }, []);
790
+
791
+ // Listens to mouse movement for dragging hotspot
792
+ useEffect(function () {
793
+ if (!isEditable) return undefined;
794
+ var containerElement = containerRef.current;
795
+ containerElement.addEventListener('mousemove', handleMouseMoveHotspot);
796
+ containerElement.addEventListener('mouseup', handleMouseUpHotspot);
797
+ return function () {
798
+ containerElement.removeEventListener('mousemove', handleMouseMoveHotspot);
799
+ containerElement.removeEventListener('mouseup', handleMouseUpHotspot);
800
+ };
801
+ }, [draggingHotspotId, handleMouseMoveHotspot, handleMouseUpHotspot, isEditable]);
639
802
  var getIconRenderFunction = useCallback(function () {
640
803
  return renderIconByName || (Array.isArray(icons) ? function (name, props) {
641
804
  var _icons$find;
@@ -654,7 +817,7 @@ var ImageHotspots = function ImageHotspots(_ref9) {
654
817
 
655
818
  // Performance improvement
656
819
  var cachedHotspots = useMemo(function () {
657
- return hotspots.map(function (hotspot, index) {
820
+ return editableHotspots.map(function (hotspot, index) {
658
821
  var _hotspot$content;
659
822
  var x = hotspot.x,
660
823
  y = hotspot.y;
@@ -664,10 +827,10 @@ var ImageHotspots = function ImageHotspots(_ref9) {
664
827
  // Determine whether the icon needs to be dynamically overridden by a threshold
665
828
  var matchingAttributeThresholds = [];
666
829
  if ((_hotspot$content = hotspot.content) !== null && _hotspot$content !== void 0 && _hotspot$content.attributes) {
667
- hotspot.content.attributes.forEach(function (_ref10) {
830
+ hotspot.content.attributes.forEach(function (_ref11) {
668
831
  var _hotspot$content2;
669
- var thresholds = _ref10.thresholds,
670
- dataSourceId = _ref10.dataSourceId;
832
+ var thresholds = _ref11.thresholds,
833
+ dataSourceId = _ref11.dataSourceId;
671
834
  if (!isEmpty(thresholds) && !isEmpty((_hotspot$content2 = hotspot.content) === null || _hotspot$content2 === void 0 ? void 0 : _hotspot$content2.values)) {
672
835
  var _hotspot$content3;
673
836
  var attributeThresholds = findMatchingThresholds(thresholds.map(function (threshold) {
@@ -693,10 +856,13 @@ var ImageHotspots = function ImageHotspots(_ref9) {
693
856
  key: "".concat(x, "-").concat(y, "-").concat(index),
694
857
  renderIconByName: getIconRenderFunction(),
695
858
  isSelected: hotspotIsSelected,
696
- onClick: onHotspotClicked
859
+ onClick: onHotspotClicked,
860
+ onMouseDown: function onMouseDown(e) {
861
+ return handleMouseDownHotspot(e, hotspot.id);
862
+ }
697
863
  }));
698
864
  });
699
- }, [hotspots, selectedHotspots, locale, getIconRenderFunction, isEditable, onHotspotContentChanged, mergedI18n, onHotspotClicked]);
865
+ }, [editableHotspots, selectedHotspots, locale, getIconRenderFunction, isEditable, onHotspotContentChanged, mergedI18n, onHotspotClicked, handleMouseDownHotspot]);
700
866
  var hotspotsStyle = {
701
867
  position: 'absolute',
702
868
  left: image.offsetX,
@@ -739,7 +905,8 @@ var ImageHotspots = function ImageHotspots(_ref9) {
739
905
  // If we leave the container, stop detecting the drag
740
906
  stopDrag(cursor, setCursor);
741
907
  }
742
- }
908
+ },
909
+ ref: containerRef
743
910
  }, src ? /*#__PURE__*/React__default.createElement("img", {
744
911
  id: id,
745
912
  className: "".concat(iotPrefix, "--image-card-img"),
@@ -966,6 +1133,17 @@ ImageHotspots.__docgenInfo = {
966
1133
  },
967
1134
  "required": false
968
1135
  },
1136
+ "onUpdateHotspotPosition": {
1137
+ "defaultValue": {
1138
+ "value": "() => {}",
1139
+ "computed": false
1140
+ },
1141
+ "description": "Callback when a hotspot is dragged to new position in isEditable mode\nEmits new hotspots and updated position obj {x, y} of hotspot.",
1142
+ "type": {
1143
+ "name": "func"
1144
+ },
1145
+ "required": false
1146
+ },
969
1147
  "onSelectHotspot": {
970
1148
  "defaultValue": {
971
1149
  "value": "() => {}",
@@ -332,6 +332,7 @@ var HotspotEditorModal = function HotspotEditorModal(_ref) {
332
332
  switchCurrentType = _useHotspotEditorStat.switchCurrentType,
333
333
  updateHotspotDataSource = _useHotspotEditorStat.updateHotspotDataSource,
334
334
  updateHotspotTooltip = _useHotspotEditorStat.updateHotspotTooltip,
335
+ updateHotspotPosition = _useHotspotEditorStat.updateHotspotPosition,
335
336
  updateTextHotspotStyle = _useHotspotEditorStat.updateTextHotspotStyle,
336
337
  updateTextHotspotContent = _useHotspotEditorStat.updateTextHotspotContent,
337
338
  updateDynamicHotspotSourceX = _useHotspotEditorStat.updateDynamicHotspotSourceX,
@@ -397,6 +398,8 @@ var HotspotEditorModal = function HotspotEditorModal(_ref) {
397
398
  }
398
399
  var hotspotsWithoutExampleValues = filteredHotspots.map(function (hotspot) {
399
400
  return update__default.default(hotspot, {
401
+ $unset: ['id'],
402
+ // Remove the internal id added for drag tracking
400
403
  content: {
401
404
  $unset: ['values']
402
405
  }
@@ -646,6 +649,7 @@ var HotspotEditorModal = function HotspotEditorModal(_ref) {
646
649
  hotspotDefaults: hotspotDefaults
647
650
  });
648
651
  },
652
+ onUpdateHotspotPosition: updateHotspotPosition,
649
653
  onSelectHotspot: setSelectedHotspot,
650
654
  selectedHotspots: getSelectedHotspotsList(selectedHotspot, hotspots),
651
655
  src: cardConfig.content.src,
@@ -77,6 +77,7 @@ var hotspotActionTypes = {
77
77
  hotspotDataSourceChange: 'HOTSPOT_DATA_SOURCE_CHANGE',
78
78
  hotspotDataSourceSettingsChange: 'HOTSPOT_DATA_SOURCE_SETTINGS_CHANGE',
79
79
  hotspotTooltipChange: 'HOTSPOT_TOOLTIP_CHANGE',
80
+ hotspotPositionChange: 'HOTSPOT_POSITION_CHANGE',
80
81
  hotspotSelect: 'HOTSPOT_SELECT',
81
82
  hotspotsAdd: 'HOTSPOTS_ADD',
82
83
  textHotspotStyleChange: 'TEXT_HOTSPOT_STYLE_CHANGE',
@@ -179,10 +180,32 @@ function hotspotEditorReducer(state, _ref2) {
179
180
  };
180
181
  return getHotspotUpdate(state, _mergeSpec);
181
182
  }
183
+ // HOTSPOT POSITION CHANGE
184
+ case hotspotActionTypes.hotspotPositionChange:
185
+ {
186
+ var isPositionAvailable = !state.hotspots.find(function (hotspot) {
187
+ return isHotspotMatch(hotspot, payload.position);
188
+ });
189
+ if (isPositionAvailable) {
190
+ // Find the updated hotspot in the new hotspots array to maintain selection
191
+ var updatedSelectedHotspot = payload.newHotspots.find(function (hotspot) {
192
+ return isHotspotMatch(hotspot, payload.position);
193
+ });
194
+ return update__default.default(state, {
195
+ hotspots: {
196
+ $set: payload.newHotspots
197
+ },
198
+ selectedHotspot: {
199
+ $set: updatedSelectedHotspot
200
+ }
201
+ });
202
+ }
203
+ return state;
204
+ }
182
205
  // HOTSPOTS ADD
183
206
  case hotspotActionTypes.hotspotsAdd:
184
207
  {
185
- var isPositionAvailable = !state.hotspots.find(function (hotspot) {
208
+ var _isPositionAvailable = !state.hotspots.find(function (hotspot) {
186
209
  return isHotspotMatch(hotspot, payload.position);
187
210
  });
188
211
  var defaultContent = state.currentType === hotspotTypes.TEXT ? {
@@ -193,7 +216,7 @@ function hotspotEditorReducer(state, _ref2) {
193
216
  content: defaultContent,
194
217
  type: createableType
195
218
  });
196
- return isPositionAvailable ? update__default.default(state, {
219
+ return _isPositionAvailable ? update__default.default(state, {
197
220
  selectedHotspot: {
198
221
  $set: newHotspot
199
222
  },
@@ -221,7 +244,7 @@ function hotspotEditorReducer(state, _ref2) {
221
244
  $set: hotspot
222
245
  },
223
246
  currentType: {
224
- $set: (_hotspot$type = hotspot.type) !== null && _hotspot$type !== void 0 ? _hotspot$type : defaultTypeWhenMissing
247
+ $set: (_hotspot$type = hotspot === null || hotspot === void 0 ? void 0 : hotspot.type) !== null && _hotspot$type !== void 0 ? _hotspot$type : defaultTypeWhenMissing
225
248
  }
226
249
  });
227
250
  }
@@ -420,6 +443,12 @@ function useHotspotEditorState() {
420
443
  payload: hotspotContent
421
444
  });
422
445
  };
446
+ var updateHotspotPosition = function updateHotspotPosition(hotspotPosition) {
447
+ return dispatch({
448
+ type: hotspotActionTypes.hotspotPositionChange,
449
+ payload: hotspotPosition
450
+ });
451
+ };
423
452
 
424
453
  /** Updates the properties of the text hotspot, passes a payload like {color: 'blue'} */
425
454
  var updateTextHotspotStyle = function updateTextHotspotStyle(textHotspotStyle) {
@@ -507,6 +536,7 @@ function useHotspotEditorState() {
507
536
  switchCurrentType: switchCurrentType,
508
537
  updateHotspotDataSource: updateHotspotDataSource,
509
538
  updateHotspotTooltip: updateHotspotTooltip,
539
+ updateHotspotPosition: updateHotspotPosition,
510
540
  updateTextHotspotStyle: updateTextHotspotStyle,
511
541
  updateTextHotspotContent: updateTextHotspotContent,
512
542
  updateDynamicHotspotSourceX: updateDynamicHotspotSourceX,
@@ -182,7 +182,7 @@ var HotspotContent = function HotspotContent(_ref) {
182
182
  }, typeof value === 'number' ? cardUtilityFunctions.formatNumberWithPrecision(value, !isNil(precision) ? precision : Math.abs(value) < 1 ? value === 0 ? 0 : 3 // for small decimals give 3 spots
183
183
  : 1,
184
184
  // otherwise 1 spot if precision isn't set
185
- locale) : value, unit && value !== '--' && /*#__PURE__*/React__default.default.createElement("span", {
185
+ locale) : typeof value === 'boolean' ? String(value) : value, unit && value !== '--' && /*#__PURE__*/React__default.default.createElement("span", {
186
186
  className: "".concat(iotPrefix, "--hotspot-content-unit")
187
187
  }, unit))));
188
188
  }));
@@ -89,6 +89,10 @@ var propTypes = {
89
89
  * Emits position obj {x, y} of hotspot to be added.
90
90
  */
91
91
  onAddHotspotPosition: PropTypes__default.default.func,
92
+ /** Callback when a hotspot is dragged to new position in isEditable mode
93
+ * Emits new hotspots and updated position obj {x, y} of hotspot.
94
+ */
95
+ onUpdateHotspotPosition: PropTypes__default.default.func,
92
96
  /** Callback when a hotspot is clicked in isEditable mode, emits position obj {x, y} */
93
97
  onSelectHotspot: PropTypes__default.default.func,
94
98
  /**
@@ -129,6 +133,7 @@ var defaultProps = {
129
133
  isHotspotDataLoading: false,
130
134
  isEditable: false,
131
135
  onAddHotspotPosition: function onAddHotspotPosition() {},
136
+ onUpdateHotspotPosition: function onUpdateHotspotPosition() {},
132
137
  onSelectHotspot: function onSelectHotspot() {},
133
138
  onHotspotContentChanged: function onHotspotContentChanged() {},
134
139
  background: '#eee',
@@ -247,6 +252,7 @@ var calculateHotspotContainerLayout = function calculateHotspotContainerLayout(_
247
252
  var width;
248
253
  var height;
249
254
  var top;
255
+ var left;
250
256
 
251
257
  // CONTAIN
252
258
  if (objectFit === 'contain') {
@@ -254,32 +260,38 @@ var calculateHotspotContainerLayout = function calculateHotspotContainerLayout(_
254
260
  width = imageWidth;
255
261
  height = imageWidth / imageRatio;
256
262
  top = imageScale > 1 ? imageOffsetY : imageObjectFitOffsetY;
263
+ left = imageScale > 1 ? 0 : (containerWidth - imageWidth) / 2;
257
264
  } else if (imageOrientation === 'portrait') {
258
265
  width = imageHeight / imageRatio;
259
266
  height = imageHeight;
260
267
  top = imageOffsetY;
268
+ left = (containerWidth - width) / 2;
261
269
  }
262
270
  // FILL
263
271
  } else if (objectFit === 'fill') {
264
272
  width = imageScale > 1 ? imageWidth : containerWidth;
265
273
  height = imageScale > 1 ? imageHeight : containerHeight;
266
274
  top = imageOffsetY;
275
+ left = 0;
267
276
  // NO OBJECT FIT
268
277
  } else if (!objectFit) {
269
278
  if (imageOrientation === 'landscape') {
270
279
  width = imageWidth;
271
280
  height = imageWidth / imageRatio;
272
281
  top = imageOffsetY;
282
+ left = 0;
273
283
  } else if (imageOrientation === 'portrait') {
274
284
  width = imageHeight / imageRatio;
275
285
  height = imageHeight;
276
286
  top = imageOffsetY;
287
+ left = (containerWidth - width) / 2;
277
288
  }
278
289
  }
279
290
  return {
280
291
  width: width,
281
292
  height: height,
282
- top: top
293
+ top: top,
294
+ left: left
283
295
  };
284
296
  };
285
297
  var calculateObjectFitOffset = function calculateObjectFitOffset(_ref5) {
@@ -524,6 +536,7 @@ var ImageHotspots = function ImageHotspots(_ref9) {
524
536
  isEditable = _ref9.isEditable,
525
537
  isHotspotDataLoading = _ref9.isHotspotDataLoading,
526
538
  onAddHotspotPosition = _ref9.onAddHotspotPosition,
539
+ onUpdateHotspotPosition = _ref9.onUpdateHotspotPosition,
527
540
  onSelectHotspot = _ref9.onSelectHotspot,
528
541
  onHotspotContentChanged = _ref9.onHotspotContentChanged,
529
542
  zoomMax = _ref9.zoomMax,
@@ -568,9 +581,52 @@ var ImageHotspots = function ImageHotspots(_ref9) {
568
581
  _useState12 = _slicedToArray__default.default(_useState11, 2),
569
582
  options = _useState12[0],
570
583
  setOptions = _useState12[1];
584
+ // Tracks if a hotspot is being dragged and which one.
585
+ var _useState13 = React.useState(null),
586
+ _useState14 = _slicedToArray__default.default(_useState13, 2),
587
+ draggingHotspotId = _useState14[0],
588
+ setDraggingHotspotId = _useState14[1];
589
+
590
+ // Ref for outer container
591
+ var containerRef = React.useRef(null);
592
+ var dragStateRef = React.useRef({
593
+ // Flag to indicate if a drag is active
594
+ isDragging: false,
595
+ // Stores the starting mouse position of a drag
596
+ dragStartPosition: {
597
+ x: 0,
598
+ y: 0
599
+ },
600
+ // Tracks the current position during a drag
601
+ currentDargPosition: null,
602
+ // Stores layout of image and container
603
+ layout: {
604
+ rect: null,
605
+ hotspotLayout: null
606
+ },
607
+ // Tracks Scheduled Animation Frames
608
+ frame: null
609
+ });
610
+
611
+ // Minimum pixel movement before a drag is initiated
612
+ var DRAG_THRESHOLD = 5;
571
613
  var mergedI18n = React.useMemo(function () {
572
614
  return _objectSpread(_objectSpread({}, defaultProps.i18n), i18n);
573
615
  }, [i18n]);
616
+ var hotspotsWithId = React.useMemo(function () {
617
+ return hotspots.map(function (hotspot, index) {
618
+ return _objectSpread(_objectSpread({}, hotspot), {}, {
619
+ id: "".concat(hotspot.x, "-").concat(hotspot.y, "-").concat(index)
620
+ });
621
+ });
622
+ }, [hotspots]);
623
+ var _useState15 = React.useState(hotspotsWithId),
624
+ _useState16 = _slicedToArray__default.default(_useState15, 2),
625
+ editableHotspots = _useState16[0],
626
+ setEditableHotspots = _useState16[1];
627
+ React.useEffect(function () {
628
+ setEditableHotspots(hotspotsWithId);
629
+ }, [hotspotsWithId]);
574
630
  var handleCtrlKeyUp = React.useCallback(function (event) {
575
631
  // Was the control key unpressed
576
632
  if (event.key === KeyCodeConstants.keyboardKeys.CONTROL) {
@@ -642,6 +698,11 @@ var ImageHotspots = function ImageHotspots(_ref9) {
642
698
  objectFit: displayOption
643
699
  };
644
700
  var onHotspotClicked = React.useCallback(function (evt, position) {
701
+ // prevent click behavior after drag
702
+ if (dragStateRef.current.isDragging) {
703
+ return;
704
+ }
705
+
645
706
  // It is possible to receive two events here, one Mouse event and one Pointer event.
646
707
  // When used in the ImageHotspots component the Pointer event can somehow be from a
647
708
  // previously clicked hotspot. See https://github.com/carbon-design-system/carbon-addons-iot-react/issues/1803
@@ -650,6 +711,108 @@ var ImageHotspots = function ImageHotspots(_ref9) {
650
711
  onSelectHotspot(position);
651
712
  }
652
713
  }, [onSelectHotspot, isEditable]);
714
+ var handleMouseDownHotspot = React.useCallback(function (e, id1) {
715
+ var hotspot = editableHotspots.find(function (h) {
716
+ return h.id === id1;
717
+ });
718
+ if (!isEditable || (hotspot === null || hotspot === void 0 ? void 0 : hotspot.type) === 'dynamic') {
719
+ return;
720
+ }
721
+ e.stopPropagation();
722
+ setDraggingHotspotId(id1);
723
+ dragStateRef.current.isDragging = true;
724
+ dragStateRef.current.dragStartPosition = {
725
+ x: e.clientX,
726
+ y: e.clientY
727
+ };
728
+
729
+ // Calculate layout once at drag start
730
+ if (containerRef.current) {
731
+ var rect = containerRef.current.getBoundingClientRect();
732
+ var hotspotLayout = calculateHotspotContainerLayout(image, container, displayOption);
733
+ dragStateRef.current.layout = {
734
+ rect: rect,
735
+ hotspotLayout: hotspotLayout
736
+ };
737
+ }
738
+ }, [container, displayOption, image, isEditable, editableHotspots]);
739
+ var handleMouseMoveHotspot = React.useCallback(function (e) {
740
+ if (draggingHotspotId !== null && containerRef.current) {
741
+ var _dragStateRef$current = dragStateRef.current,
742
+ isDragging = _dragStateRef$current.isDragging,
743
+ dragStartPosition = _dragStateRef$current.dragStartPosition,
744
+ layout = _dragStateRef$current.layout;
745
+ var dx = e.clientX - dragStartPosition.x;
746
+ var dy = e.clientY - dragStartPosition.y;
747
+ var distance = Math.sqrt(dx * dx + dy * dy);
748
+ if (distance > DRAG_THRESHOLD && isDragging) {
749
+ var rect = layout.rect,
750
+ hotspotLayout = layout.hotspotLayout;
751
+ var x = (e.clientX - rect.left - hotspotLayout.left) / hotspotLayout.width * 100;
752
+ var y = (e.clientY - rect.top - hotspotLayout.top) / hotspotLayout.height * 100;
753
+ // Clamp within image boundaries
754
+ x = Math.max(0, Math.min(100, x));
755
+ y = Math.max(0, Math.min(100, y));
756
+ dragStateRef.current.currentDargPosition = {
757
+ x: x,
758
+ y: y
759
+ };
760
+
761
+ // throttle with requestAnimationFrame
762
+ if (dragStateRef.current.frame === null) {
763
+ dragStateRef.current.frame = requestAnimationFrame(function () {
764
+ setEditableHotspots(function (prev) {
765
+ return prev.map(function (item) {
766
+ return item.id === draggingHotspotId ? _objectSpread(_objectSpread({}, item), {}, {
767
+ x: dragStateRef.current.currentDargPosition.x,
768
+ y: dragStateRef.current.currentDargPosition.y
769
+ }) : item;
770
+ });
771
+ });
772
+ dragStateRef.current.frame = null;
773
+ });
774
+ }
775
+ }
776
+ }
777
+ }, [draggingHotspotId]);
778
+ var handleMouseUpHotspot = React.useCallback(function (e) {
779
+ if (draggingHotspotId !== null && dragStateRef.current.isDragging) {
780
+ e.stopPropagation();
781
+ var _ref10 = dragStateRef.current.currentDargPosition || {},
782
+ x = _ref10.x,
783
+ y = _ref10.y;
784
+ onUpdateHotspotPosition({
785
+ newHotspots: editableHotspots,
786
+ position: {
787
+ x: x,
788
+ y: y
789
+ }
790
+ });
791
+ setDraggingHotspotId(null);
792
+ dragStateRef.current.currentDargPosition = null;
793
+ dragStateRef.current.isDragging = false;
794
+ }
795
+ }, [editableHotspots, draggingHotspotId, onUpdateHotspotPosition]);
796
+ React.useEffect(function () {
797
+ var dragState = dragStateRef.current;
798
+ return function () {
799
+ if (dragState.frame !== null) {
800
+ cancelAnimationFrame(dragState.frame);
801
+ }
802
+ };
803
+ }, []);
804
+
805
+ // Listens to mouse movement for dragging hotspot
806
+ React.useEffect(function () {
807
+ if (!isEditable) return undefined;
808
+ var containerElement = containerRef.current;
809
+ containerElement.addEventListener('mousemove', handleMouseMoveHotspot);
810
+ containerElement.addEventListener('mouseup', handleMouseUpHotspot);
811
+ return function () {
812
+ containerElement.removeEventListener('mousemove', handleMouseMoveHotspot);
813
+ containerElement.removeEventListener('mouseup', handleMouseUpHotspot);
814
+ };
815
+ }, [draggingHotspotId, handleMouseMoveHotspot, handleMouseUpHotspot, isEditable]);
653
816
  var getIconRenderFunction = React.useCallback(function () {
654
817
  return renderIconByName || (Array.isArray(icons) ? function (name, props) {
655
818
  var _icons$find;
@@ -668,7 +831,7 @@ var ImageHotspots = function ImageHotspots(_ref9) {
668
831
 
669
832
  // Performance improvement
670
833
  var cachedHotspots = React.useMemo(function () {
671
- return hotspots.map(function (hotspot, index) {
834
+ return editableHotspots.map(function (hotspot, index) {
672
835
  var _hotspot$content;
673
836
  var x = hotspot.x,
674
837
  y = hotspot.y;
@@ -678,10 +841,10 @@ var ImageHotspots = function ImageHotspots(_ref9) {
678
841
  // Determine whether the icon needs to be dynamically overridden by a threshold
679
842
  var matchingAttributeThresholds = [];
680
843
  if ((_hotspot$content = hotspot.content) !== null && _hotspot$content !== void 0 && _hotspot$content.attributes) {
681
- hotspot.content.attributes.forEach(function (_ref10) {
844
+ hotspot.content.attributes.forEach(function (_ref11) {
682
845
  var _hotspot$content2;
683
- var thresholds = _ref10.thresholds,
684
- dataSourceId = _ref10.dataSourceId;
846
+ var thresholds = _ref11.thresholds,
847
+ dataSourceId = _ref11.dataSourceId;
685
848
  if (!isEmpty(thresholds) && !isEmpty((_hotspot$content2 = hotspot.content) === null || _hotspot$content2 === void 0 ? void 0 : _hotspot$content2.values)) {
686
849
  var _hotspot$content3;
687
850
  var attributeThresholds = cardUtilityFunctions.findMatchingThresholds(thresholds.map(function (threshold) {
@@ -707,10 +870,13 @@ var ImageHotspots = function ImageHotspots(_ref9) {
707
870
  key: "".concat(x, "-").concat(y, "-").concat(index),
708
871
  renderIconByName: getIconRenderFunction(),
709
872
  isSelected: hotspotIsSelected,
710
- onClick: onHotspotClicked
873
+ onClick: onHotspotClicked,
874
+ onMouseDown: function onMouseDown(e) {
875
+ return handleMouseDownHotspot(e, hotspot.id);
876
+ }
711
877
  }));
712
878
  });
713
- }, [hotspots, selectedHotspots, locale, getIconRenderFunction, isEditable, onHotspotContentChanged, mergedI18n, onHotspotClicked]);
879
+ }, [editableHotspots, selectedHotspots, locale, getIconRenderFunction, isEditable, onHotspotContentChanged, mergedI18n, onHotspotClicked, handleMouseDownHotspot]);
714
880
  var hotspotsStyle = {
715
881
  position: 'absolute',
716
882
  left: image.offsetX,
@@ -753,7 +919,8 @@ var ImageHotspots = function ImageHotspots(_ref9) {
753
919
  // If we leave the container, stop detecting the drag
754
920
  stopDrag(cursor, setCursor);
755
921
  }
756
- }
922
+ },
923
+ ref: containerRef
757
924
  }, src ? /*#__PURE__*/React__default.default.createElement("img", {
758
925
  id: id,
759
926
  className: "".concat(iotPrefix, "--image-card-img"),
@@ -980,6 +1147,17 @@ ImageHotspots.__docgenInfo = {
980
1147
  },
981
1148
  "required": false
982
1149
  },
1150
+ "onUpdateHotspotPosition": {
1151
+ "defaultValue": {
1152
+ "value": "() => {}",
1153
+ "computed": false
1154
+ },
1155
+ "description": "Callback when a hotspot is dragged to new position in isEditable mode\nEmits new hotspots and updated position obj {x, y} of hotspot.",
1156
+ "type": {
1157
+ "name": "func"
1158
+ },
1159
+ "required": false
1160
+ },
983
1161
  "onSelectHotspot": {
984
1162
  "defaultValue": {
985
1163
  "value": "() => {}",
package/package.json CHANGED
@@ -353,7 +353,7 @@
353
353
  "whatwg-fetch": "^3.0.0"
354
354
  },
355
355
  "sideEffects": false,
356
- "version": "5.8.1",
356
+ "version": "5.8.2",
357
357
  "resolutions": {
358
358
  "chokidar": "3.3.1",
359
359
  "react-grid-layout": "1.2.2",
@@ -302625,7 +302625,7 @@ ${formatRule(Codicon.menuSubmenu)}
302625
302625
  }, typeof value === 'number' ? formatNumberWithPrecision(value, !isNil(precision) ? precision : Math.abs(value) < 1 ? value === 0 ? 0 : 3 // for small decimals give 3 spots
302626
302626
  : 1,
302627
302627
  // otherwise 1 spot if precision isn't set
302628
- locale) : value, unit && value !== '--' && /*#__PURE__*/React$1.createElement("span", {
302628
+ locale) : typeof value === 'boolean' ? String(value) : value, unit && value !== '--' && /*#__PURE__*/React$1.createElement("span", {
302629
302629
  className: "".concat(iotPrefix$Z, "--hotspot-content-unit")
302630
302630
  }, unit))));
302631
302631
  }));
@@ -302766,6 +302766,10 @@ ${formatRule(Codicon.menuSubmenu)}
302766
302766
  * Emits position obj {x, y} of hotspot to be added.
302767
302767
  */
302768
302768
  onAddHotspotPosition: PropTypes.func,
302769
+ /** Callback when a hotspot is dragged to new position in isEditable mode
302770
+ * Emits new hotspots and updated position obj {x, y} of hotspot.
302771
+ */
302772
+ onUpdateHotspotPosition: PropTypes.func,
302769
302773
  /** Callback when a hotspot is clicked in isEditable mode, emits position obj {x, y} */
302770
302774
  onSelectHotspot: PropTypes.func,
302771
302775
  /**
@@ -302806,6 +302810,7 @@ ${formatRule(Codicon.menuSubmenu)}
302806
302810
  isHotspotDataLoading: false,
302807
302811
  isEditable: false,
302808
302812
  onAddHotspotPosition: function onAddHotspotPosition() {},
302813
+ onUpdateHotspotPosition: function onUpdateHotspotPosition() {},
302809
302814
  onSelectHotspot: function onSelectHotspot() {},
302810
302815
  onHotspotContentChanged: function onHotspotContentChanged() {},
302811
302816
  background: '#eee',
@@ -302924,6 +302929,7 @@ ${formatRule(Codicon.menuSubmenu)}
302924
302929
  var width;
302925
302930
  var height;
302926
302931
  var top;
302932
+ var left;
302927
302933
 
302928
302934
  // CONTAIN
302929
302935
  if (objectFit === 'contain') {
@@ -302931,32 +302937,38 @@ ${formatRule(Codicon.menuSubmenu)}
302931
302937
  width = imageWidth;
302932
302938
  height = imageWidth / imageRatio;
302933
302939
  top = imageScale > 1 ? imageOffsetY : imageObjectFitOffsetY;
302940
+ left = imageScale > 1 ? 0 : (containerWidth - imageWidth) / 2;
302934
302941
  } else if (imageOrientation === 'portrait') {
302935
302942
  width = imageHeight / imageRatio;
302936
302943
  height = imageHeight;
302937
302944
  top = imageOffsetY;
302945
+ left = (containerWidth - width) / 2;
302938
302946
  }
302939
302947
  // FILL
302940
302948
  } else if (objectFit === 'fill') {
302941
302949
  width = imageScale > 1 ? imageWidth : containerWidth;
302942
302950
  height = imageScale > 1 ? imageHeight : containerHeight;
302943
302951
  top = imageOffsetY;
302952
+ left = 0;
302944
302953
  // NO OBJECT FIT
302945
302954
  } else if (!objectFit) {
302946
302955
  if (imageOrientation === 'landscape') {
302947
302956
  width = imageWidth;
302948
302957
  height = imageWidth / imageRatio;
302949
302958
  top = imageOffsetY;
302959
+ left = 0;
302950
302960
  } else if (imageOrientation === 'portrait') {
302951
302961
  width = imageHeight / imageRatio;
302952
302962
  height = imageHeight;
302953
302963
  top = imageOffsetY;
302964
+ left = (containerWidth - width) / 2;
302954
302965
  }
302955
302966
  }
302956
302967
  return {
302957
302968
  width: width,
302958
302969
  height: height,
302959
- top: top
302970
+ top: top,
302971
+ left: left
302960
302972
  };
302961
302973
  };
302962
302974
  var calculateObjectFitOffset = function calculateObjectFitOffset(_ref5) {
@@ -303201,6 +303213,7 @@ ${formatRule(Codicon.menuSubmenu)}
303201
303213
  isEditable = _ref9.isEditable,
303202
303214
  isHotspotDataLoading = _ref9.isHotspotDataLoading,
303203
303215
  onAddHotspotPosition = _ref9.onAddHotspotPosition,
303216
+ onUpdateHotspotPosition = _ref9.onUpdateHotspotPosition,
303204
303217
  onSelectHotspot = _ref9.onSelectHotspot,
303205
303218
  onHotspotContentChanged = _ref9.onHotspotContentChanged,
303206
303219
  zoomMax = _ref9.zoomMax,
@@ -303245,9 +303258,52 @@ ${formatRule(Codicon.menuSubmenu)}
303245
303258
  _useState12 = _slicedToArray$9(_useState11, 2),
303246
303259
  options = _useState12[0],
303247
303260
  setOptions = _useState12[1];
303261
+ // Tracks if a hotspot is being dragged and which one.
303262
+ var _useState13 = React$1.useState(null),
303263
+ _useState14 = _slicedToArray$9(_useState13, 2),
303264
+ draggingHotspotId = _useState14[0],
303265
+ setDraggingHotspotId = _useState14[1];
303266
+
303267
+ // Ref for outer container
303268
+ var containerRef = React$1.useRef(null);
303269
+ var dragStateRef = React$1.useRef({
303270
+ // Flag to indicate if a drag is active
303271
+ isDragging: false,
303272
+ // Stores the starting mouse position of a drag
303273
+ dragStartPosition: {
303274
+ x: 0,
303275
+ y: 0
303276
+ },
303277
+ // Tracks the current position during a drag
303278
+ currentDargPosition: null,
303279
+ // Stores layout of image and container
303280
+ layout: {
303281
+ rect: null,
303282
+ hotspotLayout: null
303283
+ },
303284
+ // Tracks Scheduled Animation Frames
303285
+ frame: null
303286
+ });
303287
+
303288
+ // Minimum pixel movement before a drag is initiated
303289
+ var DRAG_THRESHOLD = 5;
303248
303290
  var mergedI18n = React$1.useMemo(function () {
303249
303291
  return _objectSpread$T(_objectSpread$T({}, defaultProps$13.i18n), i18n);
303250
303292
  }, [i18n]);
303293
+ var hotspotsWithId = React$1.useMemo(function () {
303294
+ return hotspots.map(function (hotspot, index) {
303295
+ return _objectSpread$T(_objectSpread$T({}, hotspot), {}, {
303296
+ id: "".concat(hotspot.x, "-").concat(hotspot.y, "-").concat(index)
303297
+ });
303298
+ });
303299
+ }, [hotspots]);
303300
+ var _useState15 = React$1.useState(hotspotsWithId),
303301
+ _useState16 = _slicedToArray$9(_useState15, 2),
303302
+ editableHotspots = _useState16[0],
303303
+ setEditableHotspots = _useState16[1];
303304
+ React$1.useEffect(function () {
303305
+ setEditableHotspots(hotspotsWithId);
303306
+ }, [hotspotsWithId]);
303251
303307
  var handleCtrlKeyUp = React$1.useCallback(function (event) {
303252
303308
  // Was the control key unpressed
303253
303309
  if (event.key === keyboardKeys.CONTROL) {
@@ -303319,6 +303375,11 @@ ${formatRule(Codicon.menuSubmenu)}
303319
303375
  objectFit: displayOption
303320
303376
  };
303321
303377
  var onHotspotClicked = React$1.useCallback(function (evt, position) {
303378
+ // prevent click behavior after drag
303379
+ if (dragStateRef.current.isDragging) {
303380
+ return;
303381
+ }
303382
+
303322
303383
  // It is possible to receive two events here, one Mouse event and one Pointer event.
303323
303384
  // When used in the ImageHotspots component the Pointer event can somehow be from a
303324
303385
  // previously clicked hotspot. See https://github.com/carbon-design-system/carbon-addons-iot-react/issues/1803
@@ -303327,6 +303388,108 @@ ${formatRule(Codicon.menuSubmenu)}
303327
303388
  onSelectHotspot(position);
303328
303389
  }
303329
303390
  }, [onSelectHotspot, isEditable]);
303391
+ var handleMouseDownHotspot = React$1.useCallback(function (e, id1) {
303392
+ var hotspot = editableHotspots.find(function (h) {
303393
+ return h.id === id1;
303394
+ });
303395
+ if (!isEditable || (hotspot === null || hotspot === void 0 ? void 0 : hotspot.type) === 'dynamic') {
303396
+ return;
303397
+ }
303398
+ e.stopPropagation();
303399
+ setDraggingHotspotId(id1);
303400
+ dragStateRef.current.isDragging = true;
303401
+ dragStateRef.current.dragStartPosition = {
303402
+ x: e.clientX,
303403
+ y: e.clientY
303404
+ };
303405
+
303406
+ // Calculate layout once at drag start
303407
+ if (containerRef.current) {
303408
+ var rect = containerRef.current.getBoundingClientRect();
303409
+ var hotspotLayout = calculateHotspotContainerLayout(image, container, displayOption);
303410
+ dragStateRef.current.layout = {
303411
+ rect: rect,
303412
+ hotspotLayout: hotspotLayout
303413
+ };
303414
+ }
303415
+ }, [container, displayOption, image, isEditable, editableHotspots]);
303416
+ var handleMouseMoveHotspot = React$1.useCallback(function (e) {
303417
+ if (draggingHotspotId !== null && containerRef.current) {
303418
+ var _dragStateRef$current = dragStateRef.current,
303419
+ isDragging = _dragStateRef$current.isDragging,
303420
+ dragStartPosition = _dragStateRef$current.dragStartPosition,
303421
+ layout = _dragStateRef$current.layout;
303422
+ var dx = e.clientX - dragStartPosition.x;
303423
+ var dy = e.clientY - dragStartPosition.y;
303424
+ var distance = Math.sqrt(dx * dx + dy * dy);
303425
+ if (distance > DRAG_THRESHOLD && isDragging) {
303426
+ var rect = layout.rect,
303427
+ hotspotLayout = layout.hotspotLayout;
303428
+ var x = (e.clientX - rect.left - hotspotLayout.left) / hotspotLayout.width * 100;
303429
+ var y = (e.clientY - rect.top - hotspotLayout.top) / hotspotLayout.height * 100;
303430
+ // Clamp within image boundaries
303431
+ x = Math.max(0, Math.min(100, x));
303432
+ y = Math.max(0, Math.min(100, y));
303433
+ dragStateRef.current.currentDargPosition = {
303434
+ x: x,
303435
+ y: y
303436
+ };
303437
+
303438
+ // throttle with requestAnimationFrame
303439
+ if (dragStateRef.current.frame === null) {
303440
+ dragStateRef.current.frame = requestAnimationFrame(function () {
303441
+ setEditableHotspots(function (prev) {
303442
+ return prev.map(function (item) {
303443
+ return item.id === draggingHotspotId ? _objectSpread$T(_objectSpread$T({}, item), {}, {
303444
+ x: dragStateRef.current.currentDargPosition.x,
303445
+ y: dragStateRef.current.currentDargPosition.y
303446
+ }) : item;
303447
+ });
303448
+ });
303449
+ dragStateRef.current.frame = null;
303450
+ });
303451
+ }
303452
+ }
303453
+ }
303454
+ }, [draggingHotspotId]);
303455
+ var handleMouseUpHotspot = React$1.useCallback(function (e) {
303456
+ if (draggingHotspotId !== null && dragStateRef.current.isDragging) {
303457
+ e.stopPropagation();
303458
+ var _ref10 = dragStateRef.current.currentDargPosition || {},
303459
+ x = _ref10.x,
303460
+ y = _ref10.y;
303461
+ onUpdateHotspotPosition({
303462
+ newHotspots: editableHotspots,
303463
+ position: {
303464
+ x: x,
303465
+ y: y
303466
+ }
303467
+ });
303468
+ setDraggingHotspotId(null);
303469
+ dragStateRef.current.currentDargPosition = null;
303470
+ dragStateRef.current.isDragging = false;
303471
+ }
303472
+ }, [editableHotspots, draggingHotspotId, onUpdateHotspotPosition]);
303473
+ React$1.useEffect(function () {
303474
+ var dragState = dragStateRef.current;
303475
+ return function () {
303476
+ if (dragState.frame !== null) {
303477
+ cancelAnimationFrame(dragState.frame);
303478
+ }
303479
+ };
303480
+ }, []);
303481
+
303482
+ // Listens to mouse movement for dragging hotspot
303483
+ React$1.useEffect(function () {
303484
+ if (!isEditable) return undefined;
303485
+ var containerElement = containerRef.current;
303486
+ containerElement.addEventListener('mousemove', handleMouseMoveHotspot);
303487
+ containerElement.addEventListener('mouseup', handleMouseUpHotspot);
303488
+ return function () {
303489
+ containerElement.removeEventListener('mousemove', handleMouseMoveHotspot);
303490
+ containerElement.removeEventListener('mouseup', handleMouseUpHotspot);
303491
+ };
303492
+ }, [draggingHotspotId, handleMouseMoveHotspot, handleMouseUpHotspot, isEditable]);
303330
303493
  var getIconRenderFunction = React$1.useCallback(function () {
303331
303494
  return renderIconByName || (Array.isArray(icons) ? function (name, props) {
303332
303495
  var _icons$find;
@@ -303345,7 +303508,7 @@ ${formatRule(Codicon.menuSubmenu)}
303345
303508
 
303346
303509
  // Performance improvement
303347
303510
  var cachedHotspots = React$1.useMemo(function () {
303348
- return hotspots.map(function (hotspot, index) {
303511
+ return editableHotspots.map(function (hotspot, index) {
303349
303512
  var _hotspot$content;
303350
303513
  var x = hotspot.x,
303351
303514
  y = hotspot.y;
@@ -303355,10 +303518,10 @@ ${formatRule(Codicon.menuSubmenu)}
303355
303518
  // Determine whether the icon needs to be dynamically overridden by a threshold
303356
303519
  var matchingAttributeThresholds = [];
303357
303520
  if ((_hotspot$content = hotspot.content) !== null && _hotspot$content !== void 0 && _hotspot$content.attributes) {
303358
- hotspot.content.attributes.forEach(function (_ref10) {
303521
+ hotspot.content.attributes.forEach(function (_ref11) {
303359
303522
  var _hotspot$content2;
303360
- var thresholds = _ref10.thresholds,
303361
- dataSourceId = _ref10.dataSourceId;
303523
+ var thresholds = _ref11.thresholds,
303524
+ dataSourceId = _ref11.dataSourceId;
303362
303525
  if (!isEmpty(thresholds) && !isEmpty((_hotspot$content2 = hotspot.content) === null || _hotspot$content2 === void 0 ? void 0 : _hotspot$content2.values)) {
303363
303526
  var _hotspot$content3;
303364
303527
  var attributeThresholds = findMatchingThresholds(thresholds.map(function (threshold) {
@@ -303384,10 +303547,13 @@ ${formatRule(Codicon.menuSubmenu)}
303384
303547
  key: "".concat(x, "-").concat(y, "-").concat(index),
303385
303548
  renderIconByName: getIconRenderFunction(),
303386
303549
  isSelected: hotspotIsSelected,
303387
- onClick: onHotspotClicked
303550
+ onClick: onHotspotClicked,
303551
+ onMouseDown: function onMouseDown(e) {
303552
+ return handleMouseDownHotspot(e, hotspot.id);
303553
+ }
303388
303554
  }));
303389
303555
  });
303390
- }, [hotspots, selectedHotspots, locale, getIconRenderFunction, isEditable, onHotspotContentChanged, mergedI18n, onHotspotClicked]);
303556
+ }, [editableHotspots, selectedHotspots, locale, getIconRenderFunction, isEditable, onHotspotContentChanged, mergedI18n, onHotspotClicked, handleMouseDownHotspot]);
303391
303557
  var hotspotsStyle = {
303392
303558
  position: 'absolute',
303393
303559
  left: image.offsetX,
@@ -303430,7 +303596,8 @@ ${formatRule(Codicon.menuSubmenu)}
303430
303596
  // If we leave the container, stop detecting the drag
303431
303597
  stopDrag(cursor, setCursor);
303432
303598
  }
303433
- }
303599
+ },
303600
+ ref: containerRef
303434
303601
  }, src ? /*#__PURE__*/React$1.createElement("img", {
303435
303602
  id: id,
303436
303603
  className: "".concat(iotPrefix$Y, "--image-card-img"),
@@ -303657,6 +303824,17 @@ ${formatRule(Codicon.menuSubmenu)}
303657
303824
  },
303658
303825
  "required": false
303659
303826
  },
303827
+ "onUpdateHotspotPosition": {
303828
+ "defaultValue": {
303829
+ "value": "() => {}",
303830
+ "computed": false
303831
+ },
303832
+ "description": "Callback when a hotspot is dragged to new position in isEditable mode\nEmits new hotspots and updated position obj {x, y} of hotspot.",
303833
+ "type": {
303834
+ "name": "func"
303835
+ },
303836
+ "required": false
303837
+ },
303660
303838
  "onSelectHotspot": {
303661
303839
  "defaultValue": {
303662
303840
  "value": "() => {}",
@@ -316150,6 +316328,7 @@ ${formatRule(Codicon.menuSubmenu)}
316150
316328
  hotspotDataSourceChange: 'HOTSPOT_DATA_SOURCE_CHANGE',
316151
316329
  hotspotDataSourceSettingsChange: 'HOTSPOT_DATA_SOURCE_SETTINGS_CHANGE',
316152
316330
  hotspotTooltipChange: 'HOTSPOT_TOOLTIP_CHANGE',
316331
+ hotspotPositionChange: 'HOTSPOT_POSITION_CHANGE',
316153
316332
  hotspotSelect: 'HOTSPOT_SELECT',
316154
316333
  hotspotsAdd: 'HOTSPOTS_ADD',
316155
316334
  textHotspotStyleChange: 'TEXT_HOTSPOT_STYLE_CHANGE',
@@ -316252,10 +316431,32 @@ ${formatRule(Codicon.menuSubmenu)}
316252
316431
  };
316253
316432
  return getHotspotUpdate(state, _mergeSpec);
316254
316433
  }
316434
+ // HOTSPOT POSITION CHANGE
316435
+ case hotspotActionTypes.hotspotPositionChange:
316436
+ {
316437
+ var isPositionAvailable = !state.hotspots.find(function (hotspot) {
316438
+ return isHotspotMatch(hotspot, payload.position);
316439
+ });
316440
+ if (isPositionAvailable) {
316441
+ // Find the updated hotspot in the new hotspots array to maintain selection
316442
+ var updatedSelectedHotspot = payload.newHotspots.find(function (hotspot) {
316443
+ return isHotspotMatch(hotspot, payload.position);
316444
+ });
316445
+ return update(state, {
316446
+ hotspots: {
316447
+ $set: payload.newHotspots
316448
+ },
316449
+ selectedHotspot: {
316450
+ $set: updatedSelectedHotspot
316451
+ }
316452
+ });
316453
+ }
316454
+ return state;
316455
+ }
316255
316456
  // HOTSPOTS ADD
316256
316457
  case hotspotActionTypes.hotspotsAdd:
316257
316458
  {
316258
- var isPositionAvailable = !state.hotspots.find(function (hotspot) {
316459
+ var _isPositionAvailable = !state.hotspots.find(function (hotspot) {
316259
316460
  return isHotspotMatch(hotspot, payload.position);
316260
316461
  });
316261
316462
  var defaultContent = state.currentType === hotspotTypes.TEXT ? {
@@ -316266,7 +316467,7 @@ ${formatRule(Codicon.menuSubmenu)}
316266
316467
  content: defaultContent,
316267
316468
  type: createableType
316268
316469
  });
316269
- return isPositionAvailable ? update(state, {
316470
+ return _isPositionAvailable ? update(state, {
316270
316471
  selectedHotspot: {
316271
316472
  $set: newHotspot
316272
316473
  },
@@ -316294,7 +316495,7 @@ ${formatRule(Codicon.menuSubmenu)}
316294
316495
  $set: hotspot
316295
316496
  },
316296
316497
  currentType: {
316297
- $set: (_hotspot$type = hotspot.type) !== null && _hotspot$type !== void 0 ? _hotspot$type : defaultTypeWhenMissing
316498
+ $set: (_hotspot$type = hotspot === null || hotspot === void 0 ? void 0 : hotspot.type) !== null && _hotspot$type !== void 0 ? _hotspot$type : defaultTypeWhenMissing
316298
316499
  }
316299
316500
  });
316300
316501
  }
@@ -316493,6 +316694,12 @@ ${formatRule(Codicon.menuSubmenu)}
316493
316694
  payload: hotspotContent
316494
316695
  });
316495
316696
  };
316697
+ var updateHotspotPosition = function updateHotspotPosition(hotspotPosition) {
316698
+ return dispatch({
316699
+ type: hotspotActionTypes.hotspotPositionChange,
316700
+ payload: hotspotPosition
316701
+ });
316702
+ };
316496
316703
 
316497
316704
  /** Updates the properties of the text hotspot, passes a payload like {color: 'blue'} */
316498
316705
  var updateTextHotspotStyle = function updateTextHotspotStyle(textHotspotStyle) {
@@ -316580,6 +316787,7 @@ ${formatRule(Codicon.menuSubmenu)}
316580
316787
  switchCurrentType: switchCurrentType,
316581
316788
  updateHotspotDataSource: updateHotspotDataSource,
316582
316789
  updateHotspotTooltip: updateHotspotTooltip,
316790
+ updateHotspotPosition: updateHotspotPosition,
316583
316791
  updateTextHotspotStyle: updateTextHotspotStyle,
316584
316792
  updateTextHotspotContent: updateTextHotspotContent,
316585
316793
  updateDynamicHotspotSourceX: updateDynamicHotspotSourceX,
@@ -317119,6 +317327,7 @@ ${formatRule(Codicon.menuSubmenu)}
317119
317327
  switchCurrentType = _useHotspotEditorStat.switchCurrentType,
317120
317328
  updateHotspotDataSource = _useHotspotEditorStat.updateHotspotDataSource,
317121
317329
  updateHotspotTooltip = _useHotspotEditorStat.updateHotspotTooltip,
317330
+ updateHotspotPosition = _useHotspotEditorStat.updateHotspotPosition,
317122
317331
  updateTextHotspotStyle = _useHotspotEditorStat.updateTextHotspotStyle,
317123
317332
  updateTextHotspotContent = _useHotspotEditorStat.updateTextHotspotContent,
317124
317333
  updateDynamicHotspotSourceX = _useHotspotEditorStat.updateDynamicHotspotSourceX,
@@ -317184,6 +317393,8 @@ ${formatRule(Codicon.menuSubmenu)}
317184
317393
  }
317185
317394
  var hotspotsWithoutExampleValues = filteredHotspots.map(function (hotspot) {
317186
317395
  return update(hotspot, {
317396
+ $unset: ['id'],
317397
+ // Remove the internal id added for drag tracking
317187
317398
  content: {
317188
317399
  $unset: ['values']
317189
317400
  }
@@ -317433,6 +317644,7 @@ ${formatRule(Codicon.menuSubmenu)}
317433
317644
  hotspotDefaults: hotspotDefaults
317434
317645
  });
317435
317646
  },
317647
+ onUpdateHotspotPosition: updateHotspotPosition,
317436
317648
  onSelectHotspot: setSelectedHotspot,
317437
317649
  selectedHotspots: getSelectedHotspotsList(selectedHotspot, hotspots),
317438
317650
  src: cardConfig.content.src,