box-annotations 5.2.1-beta.2 → 5.2.1-beta.20

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.
Files changed (91) hide show
  1. package/dist/annotations.css +3 -4
  2. package/dist/annotations.js +1 -1
  3. package/dist/annotations.js.LICENSE.txt +3 -43
  4. package/i18n/en-US.js +4 -0
  5. package/i18n/en-US.properties +8 -0
  6. package/i18n/json/src/components/BoundingBoxHighlight/messages.json +1 -0
  7. package/i18n/json/src/components/Popups/messages.json +1 -1
  8. package/index.js +1 -1
  9. package/package.json +33 -15
  10. package/src/@types/api.ts +3 -2
  11. package/src/@types/events.ts +28 -7
  12. package/src/@types/global.ts +0 -9
  13. package/src/@types/model.ts +14 -0
  14. package/src/BoxAnnotations.ts +4 -1
  15. package/src/adapters/threadedAnnotationsAdapters.ts +164 -0
  16. package/src/api/APIFactory.ts +6 -1
  17. package/src/api/types.ts +50 -1
  18. package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +33 -0
  19. package/src/boundingBoxHighlight/BoundingBoxHighlightManager.tsx +19 -0
  20. package/src/boundingBoxHighlight/index.ts +1 -0
  21. package/src/common/AnnotationCallbacksContext.ts +7 -0
  22. package/src/common/BaseAnnotator.scss +14 -1
  23. package/src/common/BaseAnnotator.ts +161 -14
  24. package/src/common/BaseManager.ts +2 -0
  25. package/src/common/DeselectListener.tsx +3 -1
  26. package/src/common/withProviders.tsx +6 -2
  27. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.scss +8 -0
  28. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.tsx +67 -0
  29. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +73 -0
  30. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx +68 -0
  31. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +22 -0
  32. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.tsx +77 -0
  33. package/src/components/BoundingBoxHighlight/index.ts +4 -0
  34. package/src/components/BoundingBoxHighlight/messages.ts +14 -0
  35. package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
  36. package/src/components/Popups/PopupReply.tsx +8 -28
  37. package/src/components/Popups/PopupV2.scss +28 -0
  38. package/src/components/Popups/PopupV2.tsx +382 -0
  39. package/src/components/Popups/messages.ts +10 -0
  40. package/src/document/DocumentAnnotator.ts +79 -10
  41. package/src/document/docUtil.ts +2 -1
  42. package/src/drawing/DrawingAnnotations.tsx +23 -14
  43. package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
  44. package/src/drawing/DrawingCreator.tsx +9 -16
  45. package/src/drawing/DrawingManager.tsx +13 -2
  46. package/src/drawing/DrawingTarget.tsx +1 -0
  47. package/src/highlight/HighlightAnnotations.tsx +27 -14
  48. package/src/highlight/HighlightContainer.tsx +3 -0
  49. package/src/highlight/HighlightCreatorManager.ts +8 -5
  50. package/src/highlight/HighlightListener.ts +4 -2
  51. package/src/highlight/HighlightManager.tsx +13 -2
  52. package/src/highlight/HighlightTarget.tsx +1 -0
  53. package/src/icons/ChevronLeft.tsx +24 -0
  54. package/src/icons/ChevronRight.tsx +24 -0
  55. package/src/image/ImageAnnotator.ts +50 -3
  56. package/src/media/MediaAnnotator.ts +2 -1
  57. package/src/popup/PopupContainer.tsx +11 -1
  58. package/src/popup/PopupLayer.tsx +97 -22
  59. package/src/popup/PopupManager.tsx +24 -4
  60. package/src/region/RegionAnnotation.tsx +1 -0
  61. package/src/region/RegionCreation.tsx +4 -7
  62. package/src/region/RegionCreationContainer.tsx +2 -2
  63. package/src/region/RegionCreator.tsx +9 -17
  64. package/src/store/annotations/actions.ts +167 -3
  65. package/src/store/annotations/reducer.ts +71 -1
  66. package/src/store/boundingBoxHighlights/actions.ts +6 -0
  67. package/src/store/boundingBoxHighlights/index.ts +4 -0
  68. package/src/store/boundingBoxHighlights/reducer.ts +33 -0
  69. package/src/store/boundingBoxHighlights/selectors.ts +12 -0
  70. package/src/store/boundingBoxHighlights/types.ts +13 -0
  71. package/src/store/createRootReducer.ts +2 -0
  72. package/src/store/eventing/boundingBoxHighlightNav.ts +13 -0
  73. package/src/store/eventing/create.ts +3 -3
  74. package/src/store/eventing/delete.ts +27 -0
  75. package/src/store/eventing/middleware.ts +42 -3
  76. package/src/store/eventing/replyCreate.ts +34 -0
  77. package/src/store/eventing/replyDelete.ts +35 -0
  78. package/src/store/eventing/replyUpdate.ts +39 -0
  79. package/src/store/eventing/types.ts +4 -4
  80. package/src/store/eventing/update.ts +29 -0
  81. package/src/store/highlight/actions.ts +32 -3
  82. package/src/store/highlight/types.ts +1 -0
  83. package/src/store/index.ts +1 -0
  84. package/src/store/options/actions.ts +2 -0
  85. package/src/store/options/reducer.ts +9 -2
  86. package/src/store/options/selectors.ts +5 -1
  87. package/src/store/options/types.ts +7 -1
  88. package/src/store/types.ts +2 -0
  89. package/src/utils/rotate.ts +43 -0
  90. package/src/utils/scroll.ts +8 -3
  91. package/src/components/Popups/PopupReplyV2.tsx +0 -10
@@ -8,18 +8,25 @@ import {
8
8
  HighlightManager,
9
9
  isHighlight,
10
10
  } from '../highlight';
11
+ import { BoundingBoxHighlightManager } from '../boundingBoxHighlight';
11
12
  import { centerRegion, isRegion, RegionCreationManager, RegionManager } from '../region';
12
13
  import { Event } from '../@types';
13
14
  import { getAnnotation } from '../store/annotations';
15
+ import { BoundingBox } from '../store/boundingBoxHighlights';
14
16
  import { getSelection } from './docUtil';
15
17
  import { Manager } from '../common/BaseManager';
16
- import { getFileId, getIsCurrentFileVersion, Mode } from '../store';
18
+ import { getFileId, getIsCurrentFileVersion, getRotation, getViewMode, Mode } from '../store';
17
19
  import { scrollToLocation } from '../utils/scroll';
18
20
  import './DocumentAnnotator.scss';
19
21
 
22
+ import type { ViewMode } from '../store/options/types';
23
+
20
24
  // Pdf.js textLayer enhancement requires waiting for 300ms (they hardcode this magic number)
21
25
  const TEXT_LAYER_ENHANCEMENT = 300;
22
26
 
27
+ // Vertical padding on each page element as provided by Preview SDK (see DocumentAnnotator.scss)
28
+ const PAGE_PADDING = 15;
29
+
23
30
  export default class DocumentAnnotator extends BaseAnnotator {
24
31
  annotatedEl?: HTMLElement;
25
32
 
@@ -27,6 +34,9 @@ export default class DocumentAnnotator extends BaseAnnotator {
27
34
 
28
35
  managers: Map<number, Set<Manager>> = new Map();
29
36
 
37
+ /** Tracks which view mode the current managers were created for. Used to destroy/recreate when switching. */
38
+ private managersViewMode: ViewMode | null = null;
39
+
30
40
  constructor(options: Options) {
31
41
  super(options);
32
42
 
@@ -53,13 +63,22 @@ export default class DocumentAnnotator extends BaseAnnotator {
53
63
  return this.containerEl?.querySelector('.bp-doc');
54
64
  }
55
65
 
66
+ /** Destroys all managers and clears the cache. Call when switching view modes. */
67
+ private clearManagers(): void {
68
+ this.managers.forEach(managers => managers.forEach(manager => manager.destroy()));
69
+ this.managers.clear();
70
+ this.managersViewMode = null;
71
+ }
72
+
56
73
  getPageManagers(pageEl: HTMLElement): Set<Manager> {
57
74
  const fileId = getFileId(this.store.getState());
58
75
  const isCurrentFileVersion = getIsCurrentFileVersion(this.store.getState());
59
76
  const pageNumber = this.getPageNumber(pageEl);
60
77
  const pageReferenceEl = this.getPageReference(pageEl);
61
- const managers = this.managers.get(pageNumber) || new Set();
62
78
  const resinTags = { fileid: fileId, iscurrent: isCurrentFileVersion };
79
+ const viewMode = getViewMode(this.store.getState());
80
+
81
+ const managers = this.managers.get(pageNumber) || new Set();
63
82
  let destroyManagers = false;
64
83
 
65
84
  // If a referenced page element doesn't exist, destroy all managers to keep them in sync
@@ -77,8 +96,14 @@ export default class DocumentAnnotator extends BaseAnnotator {
77
96
 
78
97
  // Lazily instantiate managers as pages are added or re-rendered
79
98
  if (managers.size === 0) {
80
- managers.add(new PopupManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
81
- managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
99
+ if (viewMode === 'boundingBoxes') {
100
+ managers.add(new BoundingBoxHighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
101
+ return managers;
102
+ }
103
+
104
+ // Annotations mode
105
+ managers.add(new PopupManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
106
+ managers.add(new DrawingManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
82
107
 
83
108
  const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
84
109
 
@@ -93,8 +118,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
93
118
  );
94
119
  }
95
120
 
96
- managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
97
-
121
+ managers.add(new HighlightManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
98
122
  managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
99
123
 
100
124
  const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
@@ -132,7 +156,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
132
156
  }
133
157
 
134
158
  handleChangeMode = ({ mode }: { mode: Mode }): void => {
135
- if (!this.annotatedEl) {
159
+ if (!this.annotatedEl || getViewMode(this.store.getState()) === 'boundingBoxes') {
136
160
  return;
137
161
  }
138
162
 
@@ -146,6 +170,13 @@ export default class DocumentAnnotator extends BaseAnnotator {
146
170
  };
147
171
 
148
172
  render(): void {
173
+ const viewMode = getViewMode(this.store.getState());
174
+
175
+ if (this.managersViewMode !== null && this.managersViewMode !== viewMode) {
176
+ this.clearManagers();
177
+ }
178
+ this.managersViewMode = viewMode;
179
+
149
180
  this.getPages()
150
181
  .filter(({ dataset }) => dataset.loaded && dataset.pageNumber)
151
182
  .forEach(pageEl => this.renderPage(pageEl));
@@ -153,17 +184,51 @@ export default class DocumentAnnotator extends BaseAnnotator {
153
184
  this.postRender();
154
185
  }
155
186
 
187
+ public postRender(): void {
188
+ // DeselectManager is only needed for annotation creation; skip in bounding box mode
189
+ if (getViewMode(this.store.getState()) === 'boundingBoxes') {
190
+ if (this.deselectManager) {
191
+ this.deselectManager.destroy();
192
+ this.deselectManager = null;
193
+ }
194
+ return;
195
+ }
196
+ super.postRender();
197
+ }
198
+
156
199
  renderPage(pageEl: HTMLElement): void {
157
200
  const pageManagers = this.getPageManagers(pageEl);
158
201
  const pageNumber = this.getPageNumber(pageEl);
202
+ const rotation = getRotation(this.store.getState()) || 0;
203
+
204
+ // Calculate original (unrotated) page dimensions for annotation layer sizing
205
+ const pageWidth = pageEl.clientWidth;
206
+ const pageHeight = pageEl.clientHeight - PAGE_PADDING * 2;
207
+ const isOrthogonal = rotation % 180 !== 0;
208
+ const origWidth = isOrthogonal ? pageHeight : pageWidth;
209
+ const origHeight = isOrthogonal ? pageWidth : pageHeight;
159
210
 
160
211
  // Render annotations for every page
161
- pageManagers.forEach(manager =>
212
+ pageManagers.forEach(manager => {
213
+ // Apply rotation transform to annotation layers
214
+ if (rotation) {
215
+ manager.style({
216
+ height: `${origHeight}px`,
217
+ left: '50%',
218
+ top: '50%',
219
+ transform: `translate(-50%, -50%) rotate(${rotation}deg)`,
220
+ width: `${origWidth}px`,
221
+ });
222
+ } else {
223
+ manager.style({height: '', left: '', top: '', transform: '', width: ''});
224
+ }
225
+
162
226
  manager.render({
227
+ callbacks: { onCopyLink: this.onCopyLink },
163
228
  intl: this.intl,
164
229
  store: this.store,
165
- }),
166
- );
230
+ });
231
+ });
167
232
 
168
233
  this.managers.set(pageNumber, pageManagers);
169
234
  }
@@ -194,4 +259,8 @@ export default class DocumentAnnotator extends BaseAnnotator {
194
259
  scrollToLocation(this.annotatedEl, annotationPageEl, { offsets });
195
260
  }
196
261
  }
262
+
263
+ protected getScrollReferenceForHighlight(highlight: BoundingBox): HTMLElement | undefined {
264
+ return this.getPage(highlight.pageNumber);
265
+ }
197
266
  }
@@ -1,4 +1,5 @@
1
1
  type Selection = {
2
+ containerEl: HTMLElement;
2
3
  containerRect: DOMRect;
3
4
  hasError?: boolean;
4
5
  location: number;
@@ -61,5 +62,5 @@ export function getSelection(): Selection | null {
61
62
  location = getPageNumber(selection.focusNode as Element) ?? endPage;
62
63
  }
63
64
 
64
- return { containerRect: containerEl.getBoundingClientRect(), hasError, location, range };
65
+ return { containerEl: containerEl as HTMLElement, containerRect: containerEl.getBoundingClientRect(), hasError, location, range };
65
66
  }
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import ReactDOM from 'react-dom';
2
3
  import classNames from 'classnames';
3
4
  import DecoratedDrawingPath from './DecoratedDrawingPath';
4
5
  import DrawingList from './DrawingList';
@@ -22,8 +23,10 @@ export type Props = {
22
23
  drawnPathGroups: Array<PathGroup>;
23
24
  isCreating: boolean;
24
25
  location: number;
26
+ popupPortalEl?: HTMLElement | null;
25
27
  redoDrawingPathGroup: () => void;
26
28
  resetDrawing: () => void;
29
+ rotation: number;
27
30
  setActiveAnnotationId: (annotationId: string | null) => void;
28
31
  setReferenceId: (uuid: string) => void;
29
32
  setStaged: (staged: CreatorItemDrawing | null) => void;
@@ -45,8 +48,10 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
45
48
  drawnPathGroups,
46
49
  isCreating,
47
50
  location,
51
+ popupPortalEl,
48
52
  redoDrawingPathGroup,
49
53
  resetDrawing,
54
+ rotation,
50
55
  setActiveAnnotationId,
51
56
  setReferenceId,
52
57
  setStaged,
@@ -170,23 +175,27 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
170
175
  color={color}
171
176
  onStart={handleStart}
172
177
  onStop={handleStop}
178
+ rotation={rotation}
173
179
  />
174
180
  )}
175
181
 
176
- {isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (
177
- <PopupDrawingToolbar
178
- ref={popupDrawingToolbarRef}
179
- canComment={hasDrawnPathGroups}
180
- canRedo={hasStashedPathGroups}
181
- canUndo={hasDrawnPathGroups}
182
- className={classNames('ba-DrawingAnnotations-toolbar', { 'ba-is-drawing': isDrawing })}
183
- onDelete={handleDelete}
184
- onRedo={handleRedo}
185
- onReply={handleReply}
186
- onUndo={handleUndo}
187
- reference={drawingSVGGroup}
188
- />
189
- )}
182
+ {isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (() => {
183
+ const toolbar = (
184
+ <PopupDrawingToolbar
185
+ ref={popupDrawingToolbarRef}
186
+ canComment={hasDrawnPathGroups}
187
+ canRedo={hasStashedPathGroups}
188
+ canUndo={hasDrawnPathGroups}
189
+ className={classNames('ba-DrawingAnnotations-toolbar', { 'ba-is-drawing': isDrawing })}
190
+ onDelete={handleDelete}
191
+ onRedo={handleRedo}
192
+ onReply={handleReply}
193
+ onUndo={handleUndo}
194
+ reference={drawingSVGGroup}
195
+ />
196
+ );
197
+ return popupPortalEl ? ReactDOM.createPortal(toolbar, popupPortalEl) : toolbar;
198
+ })()}
190
199
  </>
191
200
  );
192
201
  };
@@ -18,6 +18,7 @@ import {
18
18
  getAnnotationsForLocation,
19
19
  getColor,
20
20
  getCreatorStatus,
21
+ getRotation,
21
22
  Mode,
22
23
  setActiveAnnotationIdAction,
23
24
  setReferenceIdAction,
@@ -34,6 +35,7 @@ export type Props = {
34
35
  canShowPopupToolbar: boolean;
35
36
  drawnPathGroups: Array<PathGroup>;
36
37
  isCreating: boolean;
38
+ rotation: number;
37
39
  stashedPathGroups: Array<PathGroup>;
38
40
  };
39
41
 
@@ -47,6 +49,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
47
49
  canShowPopupToolbar: creatorStatus === CreatorStatus.started,
48
50
  drawnPathGroups: getDrawingDrawnPathGroupsForLocation(state, location),
49
51
  isCreating: getAnnotationMode(state) === Mode.DRAWING && creatorStatus !== CreatorStatus.pending,
52
+ rotation: getRotation(state),
50
53
  stashedPathGroups: getStashedDrawnPathGroupsForLocation(state, location),
51
54
  };
52
55
  };
@@ -7,6 +7,7 @@ import DrawingSVG, { DrawingSVGRef } from './DrawingSVG';
7
7
  import PointerCapture, { PointerCaptureRef, Status as DrawingStatus } from '../components/PointerCapture';
8
8
  import { getDrawingCursor } from './DrawingCursor';
9
9
  import { getPathCommands } from './drawingUtil';
10
+ import { getElementLocalPosition } from '../utils/rotate';
10
11
  import { PathGroup, Position } from '../@types';
11
12
  import './DrawingCreator.scss';
12
13
 
@@ -15,6 +16,7 @@ export type Props = {
15
16
  color?: string;
16
17
  onStart: () => void;
17
18
  onStop: (pathGroup: PathGroup) => void;
19
+ rotation?: number;
18
20
  size?: number;
19
21
  };
20
22
 
@@ -26,6 +28,7 @@ export default function DrawingCreator({
26
28
  color = defaultStrokeColor,
27
29
  onStart,
28
30
  onStop,
31
+ rotation = 0,
29
32
  size = defaultStrokeSize,
30
33
  }: Props): JSX.Element {
31
34
  const [drawingStatus, setDrawingStatus] = React.useState<DrawingStatus>(DrawingStatus.init);
@@ -45,7 +48,9 @@ export default function DrawingCreator({
45
48
  return [];
46
49
  }
47
50
 
48
- const { height, width } = creatorEl.getBoundingClientRect();
51
+ // Get the element's dimensions (before any rotation is applied)
52
+ const width = creatorEl.offsetWidth;
53
+ const height = creatorEl.offsetHeight;
49
54
  const { size: minSize } = stroke;
50
55
  const MAX_X = width - minSize;
51
56
  const MAX_Y = height - minSize;
@@ -56,21 +61,9 @@ export default function DrawingCreator({
56
61
  }));
57
62
  }, [stroke]);
58
63
 
59
- const getPosition = (x: number, y: number): [number, number] => {
60
- const { current: creatorEl } = creatorElRef;
61
-
62
- if (!creatorEl) {
63
- return [x, y];
64
- }
65
-
66
- // Calculate the new position based on the mouse position less the page offset
67
- const { left, top } = creatorEl.getBoundingClientRect();
68
- return [x - left, y - top];
69
- };
70
-
71
64
  // Drawing Lifecycle Callbacks
72
65
  const startDraw = (x: number, y: number): void => {
73
- const [x1, y1] = getPosition(x, y);
66
+ const [x1, y1] = getElementLocalPosition(x, y, creatorElRef.current, rotation);
74
67
 
75
68
  setDrawingStatus(DrawingStatus.dragging);
76
69
 
@@ -100,7 +93,7 @@ export default function DrawingCreator({
100
93
 
101
94
  const updateDraw = React.useCallback(
102
95
  (x: number, y: number): void => {
103
- const [x2, y2] = getPosition(x, y);
96
+ const [x2, y2] = getElementLocalPosition(x, y, creatorElRef.current, rotation);
104
97
  const { current: points } = capturedPointsRef;
105
98
 
106
99
  points.push({ x: x2, y: y2 });
@@ -111,7 +104,7 @@ export default function DrawingCreator({
111
104
  onStart();
112
105
  }
113
106
  },
114
- [drawingStatus, onStart, setDrawingStatus],
107
+ [drawingStatus, onStart, rotation, setDrawingStatus],
115
108
  );
116
109
 
117
110
  // Event Handlers
@@ -1,9 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import * as ReactDOM from 'react-dom/client';
3
- import BaseManager, { Props } from '../common/BaseManager';
3
+ import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
4
4
  import DrawingAnnotationsContainer from './DrawingAnnotationsContainer';
5
5
 
6
+ export type Options = BaseOptions & {
7
+ popupPortalEl?: HTMLElement | null;
8
+ };
9
+
6
10
  export default class DrawingListManager extends BaseManager {
11
+ popupPortalEl?: HTMLElement | null;
12
+
13
+ constructor({ popupPortalEl, ...options }: Options) {
14
+ super(options);
15
+ this.popupPortalEl = popupPortalEl;
16
+ }
17
+
7
18
  decorate(): void {
8
19
  this.reactEl.classList.add('ba-Layer--drawing');
9
20
  this.reactEl.dataset.testid = 'ba-Layer--drawing';
@@ -14,6 +25,6 @@ export default class DrawingListManager extends BaseManager {
14
25
  this.root = ReactDOM.createRoot(this.reactEl);
15
26
  }
16
27
 
17
- this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} {...props} />);
28
+ this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
18
29
  }
19
30
  }
@@ -64,6 +64,7 @@ export const DrawingTarget = (props: Props, ref: React.Ref<DrawingTargetRef>): J
64
64
  <a
65
65
  ref={ref}
66
66
  className={classNames('ba-DrawingTarget', className, { 'is-active': isActive })}
67
+ data-ba-annotation-id={annotationId}
67
68
  data-resin-itemid={annotationId}
68
69
  data-resin-target="highlightDrawing"
69
70
  data-testid={`ba-AnnotationTarget-${annotationId}`}
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import ReactDOM from 'react-dom';
2
3
  import HighlightCanvas from './HighlightCanvas';
3
4
  import HighlightList from './HighlightList';
4
5
  import HighlightSvg from './HighlightSvg';
@@ -20,6 +21,8 @@ type Props = {
20
21
  isPromoting: boolean;
21
22
  isSelecting: boolean;
22
23
  location: number;
24
+ popupPortalEl?: HTMLElement | null;
25
+ rotation: number;
23
26
  selection: SelectionItem | null;
24
27
  setActiveAnnotationId: (annotationId: string | null) => void;
25
28
  setIsPromoting: (isPromoting: boolean) => void;
@@ -38,6 +41,8 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
38
41
  isPending = false,
39
42
  isPromoting = false,
40
43
  isSelecting = false,
44
+ popupPortalEl,
45
+ rotation,
41
46
  selection,
42
47
  setActiveAnnotationId,
43
48
  setIsPromoting,
@@ -121,22 +126,30 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
121
126
  )}
122
127
 
123
128
  {/* Layer 3a: Annotations promoter to promote selection to staged */}
124
- {!isCreating && selection && !selection.hasError && (
125
- <div className="ba-HighlightAnnotations-popup">
126
- <PopupHighlight
127
- onCancel={handleCancel}
128
- onSubmit={handlePromote}
129
- shape={getBoundingRect(selection.rects)}
130
- />
131
- </div>
132
- )}
129
+ {!isCreating && selection && !selection.hasError && (() => {
130
+ const popupShape = selection.popupRect || getBoundingRect(selection.rects);
131
+ const popup = (
132
+ <div className="ba-HighlightAnnotations-popup">
133
+ <PopupHighlight
134
+ onCancel={handleCancel}
135
+ onSubmit={handlePromote}
136
+ shape={popupShape}
137
+ />
138
+ </div>
139
+ );
140
+ return rotation && popupPortalEl ? ReactDOM.createPortal(popup, popupPortalEl) : popup;
141
+ })()}
133
142
 
134
143
  {/* Layer 3b: Highlight error popup */}
135
- {selection && selection.hasError && (
136
- <div className="ba-HighlightAnnotations-popup">
137
- <PopupHighlightError onCancel={handleCancel} shape={getBoundingRect(selection.rects)} />
138
- </div>
139
- )}
144
+ {selection && selection.hasError && (() => {
145
+ const popupShape = selection.popupRect || getBoundingRect(selection.rects);
146
+ const popup = (
147
+ <div className="ba-HighlightAnnotations-popup">
148
+ <PopupHighlightError onCancel={handleCancel} shape={popupShape} />
149
+ </div>
150
+ );
151
+ return rotation && popupPortalEl ? ReactDOM.createPortal(popup, popupPortalEl) : popup;
152
+ })()}
140
153
  </>
141
154
  );
142
155
  };
@@ -13,6 +13,7 @@ import {
13
13
  getCreatorStatus,
14
14
  getIsPromoting,
15
15
  getIsSelecting,
16
+ getRotation,
16
17
  getSelectionForLocation,
17
18
  isCreatorStagedHighlight,
18
19
  Mode,
@@ -33,6 +34,7 @@ export type Props = {
33
34
  isPending: boolean;
34
35
  isPromoting: boolean;
35
36
  isSelecting: boolean;
37
+ rotation: number;
36
38
  selection: SelectionItem | null;
37
39
  staged: CreatorItemHighlight | null;
38
40
  };
@@ -47,6 +49,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
47
49
  isPending: getCreatorStatus(state) === CreatorStatus.pending,
48
50
  isPromoting: getIsPromoting(state),
49
51
  isSelecting: getIsSelecting(state),
52
+ rotation: getRotation(state),
50
53
  selection: getSelectionForLocation(state, location),
51
54
  staged: isCreatorStagedHighlight(staged) ? staged : null,
52
55
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AppStore,
3
3
  getIsSelecting,
4
+ getRotation,
4
5
  SelectionArg as Selection,
5
6
  setIsSelectingAction,
6
7
  setSelectionAction,
@@ -41,18 +42,18 @@ export default class HighlightCreatorManager implements Manager {
41
42
  this.store.dispatch(setSelectionAction(null));
42
43
 
43
44
  this.referenceEl.addEventListener('mousedown', this.handleMouseDown);
44
- this.referenceEl.addEventListener('mouseup', this.handleMouseUp);
45
+ document.addEventListener('mouseup', this.handleMouseUp);
45
46
  }
46
47
 
47
- style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration {
48
- return Object.assign(this.referenceEl.style, styles);
48
+ style(): CSSStyleDeclaration {
49
+ return this.referenceEl.style;
49
50
  }
50
51
 
51
52
  destroy(): void {
52
53
  clearTimeout(this.selectionChangeTimer);
53
54
 
54
55
  this.referenceEl.removeEventListener('mousedown', this.handleMouseDown);
55
- this.referenceEl.removeEventListener('mouseup', this.handleMouseUp);
56
+ document.removeEventListener('mouseup', this.handleMouseUp);
56
57
  }
57
58
 
58
59
  handleMouseDown = ({ buttons }: MouseEvent): void => {
@@ -72,7 +73,9 @@ export default class HighlightCreatorManager implements Manager {
72
73
  }
73
74
 
74
75
  this.selectionChangeTimer = window.setTimeout(() => {
75
- this.store.dispatch(setSelectionAction(this.getSelection()));
76
+ const selection = this.getSelection();
77
+ const rotation = getRotation(this.store.getState());
78
+ this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
76
79
  this.store.dispatch(setIsSelectingAction(false));
77
80
  }, this.selectionChangeDelay);
78
81
  };
@@ -1,5 +1,5 @@
1
1
  import debounce from 'lodash/debounce';
2
- import { AppStore, getIsSelecting, SelectionArg as Selection, setSelectionAction } from '../store';
2
+ import { AppStore, getIsSelecting, getRotation, SelectionArg as Selection, setSelectionAction } from '../store';
3
3
 
4
4
  export type Options = {
5
5
  getSelection: () => Selection | null;
@@ -30,7 +30,9 @@ export default class HighlightListener {
30
30
  return;
31
31
  }
32
32
 
33
- this.store.dispatch(setSelectionAction(this.getSelection()));
33
+ const selection = this.getSelection();
34
+ const rotation = getRotation(this.store.getState());
35
+ this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
34
36
  };
35
37
 
36
38
  debounceHandleSelectionChange = debounce(this.handleSelectionChange, SELECTION_CHANGE_DEBOUNCE);
@@ -1,9 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import * as ReactDOM from 'react-dom/client';
3
- import BaseManager, { Props } from '../common/BaseManager';
3
+ import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
4
4
  import HighlightContainer from './HighlightContainer';
5
5
 
6
+ export type Options = BaseOptions & {
7
+ popupPortalEl?: HTMLElement | null;
8
+ };
9
+
6
10
  export default class HighlightManager extends BaseManager {
11
+ popupPortalEl?: HTMLElement | null;
12
+
13
+ constructor({ popupPortalEl, ...options }: Options) {
14
+ super(options);
15
+ this.popupPortalEl = popupPortalEl;
16
+ }
17
+
7
18
  decorate(): void {
8
19
  this.reactEl.classList.add('ba-Layer--highlight');
9
20
  this.reactEl.dataset.testid = 'ba-Layer--highlight';
@@ -14,6 +25,6 @@ export default class HighlightManager extends BaseManager {
14
25
  this.root = ReactDOM.createRoot(this.reactEl);
15
26
  }
16
27
 
17
- this.root.render(<HighlightContainer location={this.location} {...props} />);
28
+ this.root.render(<HighlightContainer location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
18
29
  }
19
30
  }
@@ -69,6 +69,7 @@ const HighlightTarget = (props: Props, ref: React.Ref<HighlightTargetRef>): JSX.
69
69
  <a
70
70
  ref={ref}
71
71
  className={classNames('ba-HighlightTarget', className, { 'is-active': isActive })}
72
+ data-ba-annotation-id={annotationId}
72
73
  data-ba-reference-id={uuid}
73
74
  data-resin-itemid={annotationId}
74
75
  data-resin-target="highlightText"
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ function ChevronLeft(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ChevronLeft"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 24 24"
12
+ width="1em"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ {...props}
15
+ >
16
+ <path
17
+ d="M13.293 4.293a1 1 0 1 1 1.414 1.414L8.414 12l6.293 6.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068L7 13.414a2 2 0 0 1 0-2.828l6.293-6.293Z"
18
+ fill="currentColor"
19
+ />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ export default ChevronLeft;
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ function ChevronRight(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ChevronRight"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 24 24"
12
+ width="1em"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ {...props}
15
+ >
16
+ <path
17
+ d="M9.293 4.293a1 1 0 0 1 1.414 0L17 10.586a2 2 0 0 1 0 2.828l-6.293 6.293a1 1 0 0 1-1.414-1.414L15.586 12 9.293 5.707a1 1 0 0 1 0-1.414Z"
18
+ fill="currentColor"
19
+ />
20
+ </svg>
21
+ );
22
+ }
23
+
24
+ export default ChevronRight;