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
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom/client';
3
+ import BaseManager, { Props } from '../common/BaseManager';
4
+ import BoundingBoxHighlightContainer from './BoundingBoxHighlightContainer';
5
+
6
+ export default class BoundingBoxHighlightManager extends BaseManager {
7
+ decorate(): void {
8
+ this.reactEl.classList.add('ba-Layer--boundingBoxHighlight');
9
+ this.reactEl.dataset.testid = 'ba-Layer--boundingBoxHighlight';
10
+ }
11
+
12
+ render(props: Props): void {
13
+ if (!this.root) {
14
+ this.root = ReactDOM.createRoot(this.reactEl);
15
+ }
16
+
17
+ this.root.render(<BoundingBoxHighlightContainer location={this.location} {...props} />);
18
+ }
19
+ }
@@ -0,0 +1 @@
1
+ export { default as BoundingBoxHighlightManager } from './BoundingBoxHighlightManager';
@@ -0,0 +1,7 @@
1
+ import { createContext } from 'react';
2
+
3
+ export type AnnotationCallbacks = {
4
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
5
+ };
6
+
7
+ export default createContext<AnnotationCallbacks>({});
@@ -7,7 +7,8 @@
7
7
  @import '~box-ui-elements/es/styles/common/buttons';
8
8
 
9
9
  &.is-hidden {
10
- .ba-Layer {
10
+ .ba-Layer,
11
+ .ba-PopupPortal {
11
12
  display: none;
12
13
  }
13
14
  }
@@ -27,3 +28,15 @@
27
28
  .ba-Layer--popup {
28
29
  z-index: 4;
29
30
  }
31
+
32
+ .ba-PopupPortal {
33
+ position: relative;
34
+ z-index: 10;
35
+ pointer-events: none;
36
+
37
+ .ba-Popup,
38
+ .ba-PopupV2 {
39
+ z-index: 150;
40
+ pointer-events: auto;
41
+ }
42
+ }
@@ -6,8 +6,11 @@ import DeselectManager from './DeselectManager';
6
6
  import EventEmitter from './EventEmitter';
7
7
  import i18n from '../utils/i18n';
8
8
  import messages from '../messages';
9
- import { Event, IntlOptions, LegacyEvent, Permissions } from '../@types';
9
+ import { Event, IntlOptions, LegacyEvent, Permissions, SidebarEvent, Token } from '../@types';
10
+ import { BoundingBox, getBoundingBoxHighlights } from '../store/boundingBoxHighlights';
11
+ import { ViewMode } from '../store/options/types';
10
12
  import { Features } from '../BoxAnnotations';
13
+ import { scrollToLocation } from '../utils/scroll';
11
14
  import './BaseAnnotator.scss';
12
15
 
13
16
  export type Container = string | HTMLElement;
@@ -37,13 +40,17 @@ export type Options = {
37
40
  hasTouch?: boolean;
38
41
  initialColor?: string;
39
42
  initialMode?: store.Mode;
43
+ /** View mode: annotations and bounding boxes are mutually exclusive. Defaults to 'annotations'. */
44
+ initialViewMode?: ViewMode;
40
45
  intl: IntlOptions;
41
46
  locale?: string;
42
- token: string;
47
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
48
+ token: Token;
43
49
  };
44
50
 
45
51
  export const CSS_CONTAINER_CLASS = 'ba';
46
52
  export const CSS_LOADED_CLASS = 'ba-annotations-loaded';
53
+ export const CSS_POPUP_PORTAL_CLASS = 'ba-PopupPortal';
47
54
  export const ANNOTATION_CLASSES: { [M in store.Mode]?: string } = {
48
55
  [store.Mode.HIGHLIGHT]: 'ba-is-create--highlight',
49
56
  [store.Mode.REGION]: 'ba-is-create--region',
@@ -54,6 +61,8 @@ export default class BaseAnnotator extends EventEmitter {
54
61
 
55
62
  containerEl?: HTMLElement | null;
56
63
 
64
+ popupPortalEl?: HTMLElement | null;
65
+
57
66
  container: Container;
58
67
 
59
68
  deselectManager: DeselectManager | null = null;
@@ -62,6 +71,8 @@ export default class BaseAnnotator extends EventEmitter {
62
71
 
63
72
  intl: IntlShape;
64
73
 
74
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
75
+
65
76
  store: store.AppStore;
66
77
 
67
78
  constructor({
@@ -72,7 +83,9 @@ export default class BaseAnnotator extends EventEmitter {
72
83
  fileOptions,
73
84
  initialColor,
74
85
  initialMode,
86
+ initialViewMode = 'annotations',
75
87
  intl,
88
+ onCopyLink,
76
89
  token,
77
90
  }: Options) {
78
91
  super();
@@ -88,17 +101,21 @@ export default class BaseAnnotator extends EventEmitter {
88
101
  },
89
102
  common: { color: initialColor, mode: initialMode },
90
103
  options: {
104
+ apiHost,
91
105
  features,
92
106
  fileId: file.id,
93
107
  fileVersionId: fileOptionsVersionId ?? fileVersionId,
94
108
  isCurrentFileVersion: !fileOptionsVersionId || fileOptionsVersionId === currentFileVersionId,
95
109
  permissions: file.permissions,
110
+ token,
111
+ viewMode: initialViewMode,
96
112
  },
97
113
  };
98
114
 
99
115
  this.container = container;
100
116
  this.features = features;
101
117
  this.intl = i18n.createIntlProvider(intl);
118
+ this.onCopyLink = onCopyLink;
102
119
  this.store = store.createStore(initialState, {
103
120
  api: new API({ apiHost, token }),
104
121
  });
@@ -109,6 +126,15 @@ export default class BaseAnnotator extends EventEmitter {
109
126
  this.addListener(Event.ANNOTATION_REMOVE, this.handleRemove);
110
127
  this.addListener(Event.COLOR_SET, this.handleSetColor);
111
128
  this.addListener(Event.VISIBLE_SET, this.handleSetVisible);
129
+ this.addListener(Event.BOUNDING_BOX_HIGHLIGHTS_SET, this.handleSetBoundingBoxHighlights);
130
+ this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
131
+ this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
132
+ this.addListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
133
+
134
+ this.addListener(SidebarEvent.SIDEBAR_ANNOTATION_UPDATE, this.handleSidebarAnnotationUpdate);
135
+ this.addListener(SidebarEvent.SIDEBAR_REPLY_CREATE, this.handleSidebarReplyCreate);
136
+ this.addListener(SidebarEvent.SIDEBAR_REPLY_UPDATE, this.handleSidebarReplyUpdate);
137
+ this.addListener(SidebarEvent.SIDEBAR_REPLY_DELETE, this.handleSidebarReplyDelete);
112
138
 
113
139
  // Load any required data at startup
114
140
  this.hydrate();
@@ -119,6 +145,11 @@ export default class BaseAnnotator extends EventEmitter {
119
145
  this.containerEl.classList.remove(CSS_CONTAINER_CLASS);
120
146
  }
121
147
 
148
+ if (this.popupPortalEl) {
149
+ this.popupPortalEl.remove();
150
+ this.popupPortalEl = null;
151
+ }
152
+
122
153
  if (this.annotatedEl) {
123
154
  this.annotatedEl.classList.remove(CSS_LOADED_CLASS);
124
155
  }
@@ -134,14 +165,14 @@ export default class BaseAnnotator extends EventEmitter {
134
165
  this.removeListener(Event.ANNOTATION_REMOVE, this.handleRemove);
135
166
  this.removeListener(Event.COLOR_SET, this.handleSetColor);
136
167
  this.removeListener(Event.VISIBLE_SET, this.handleSetVisible);
137
- }
138
-
139
- public getActiveId(): string | null {
140
- return store.getActiveAnnotationId(this.store.getState());
141
- }
142
-
143
- public getAnnotationContainerEl(): HTMLElement | null {
144
- return this.containerEl ?? null;
168
+ this.removeListener(Event.BOUNDING_BOX_HIGHLIGHTS_SET, this.handleSetBoundingBoxHighlights);
169
+ this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
170
+ this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
171
+ this.removeListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
172
+ this.removeListener(SidebarEvent.SIDEBAR_ANNOTATION_UPDATE, this.handleSidebarAnnotationUpdate);
173
+ this.removeListener(SidebarEvent.SIDEBAR_REPLY_CREATE, this.handleSidebarReplyCreate);
174
+ this.removeListener(SidebarEvent.SIDEBAR_REPLY_UPDATE, this.handleSidebarReplyUpdate);
175
+ this.removeListener(SidebarEvent.SIDEBAR_REPLY_DELETE, this.handleSidebarReplyDelete);
145
176
  }
146
177
 
147
178
  public init(scale = 1, rotation = 0): void {
@@ -157,6 +188,12 @@ export default class BaseAnnotator extends EventEmitter {
157
188
  this.annotatedEl.classList.add(CSS_LOADED_CLASS);
158
189
  this.containerEl.classList.add(CSS_CONTAINER_CLASS);
159
190
 
191
+ if (!this.popupPortalEl) {
192
+ this.popupPortalEl = this.containerEl.ownerDocument.createElement('div');
193
+ this.popupPortalEl.classList.add(CSS_POPUP_PORTAL_CLASS);
194
+ this.containerEl.appendChild(this.popupPortalEl);
195
+ }
196
+
160
197
  // Defer to the child class to render annotations
161
198
  this.render();
162
199
 
@@ -170,10 +207,6 @@ export default class BaseAnnotator extends EventEmitter {
170
207
  return getProp(this.features, feature, false);
171
208
  }
172
209
 
173
- public isPopupOpen(): boolean {
174
- return !!this.containerEl?.querySelector('.ba-Popup');
175
- }
176
-
177
210
  public removeAnnotation = (annotationId: string): void => {
178
211
  this.store.dispatch(store.removeAnnotationAction(annotationId));
179
212
  };
@@ -183,6 +216,88 @@ export default class BaseAnnotator extends EventEmitter {
183
216
  // Called by box-content-preview
184
217
  }
185
218
 
219
+ public scrollToBoundingBoxHighlight(highlightId: string | null): void {
220
+ if (!highlightId || !this.annotatedEl) {
221
+ return;
222
+ }
223
+
224
+ const highlights = getBoundingBoxHighlights(this.store.getState());
225
+ const highlight = highlights.find((h: BoundingBox) => h.id === highlightId);
226
+
227
+ if (!highlight) {
228
+ return;
229
+ }
230
+
231
+ const referenceEl = this.getScrollReferenceForHighlight(highlight);
232
+ if (!referenceEl) {
233
+ return;
234
+ }
235
+
236
+ const offsets = {
237
+ x: highlight.x + highlight.width / 2,
238
+ y: highlight.y + highlight.height / 2,
239
+ };
240
+
241
+ scrollToLocation(this.annotatedEl, referenceEl, {
242
+ offsets: this.getAdjustedScrollOffsets(offsets),
243
+ smooth: true,
244
+ });
245
+ }
246
+
247
+ /** Returns the element to scroll relative to for the given bounding box highlight. */
248
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
249
+ protected getScrollReferenceForHighlight(_highlight: BoundingBox): HTMLElement | null | undefined {
250
+ return undefined; // Must be implemented in child class
251
+ }
252
+
253
+ /** Adjusts scroll offsets before passing to scrollToLocation (e.g. to apply rotation). */
254
+ protected getAdjustedScrollOffsets(offsets: { x: number; y: number }): { x: number; y: number } {
255
+ return offsets;
256
+ }
257
+
258
+ /**
259
+ * Sets the bounding box highlights to display. This replaces all existing bounding boxes.
260
+ * Triggers a re-render.
261
+ *
262
+ * Note: Bounding boxes are only visible when viewMode is set to 'boundingBoxes'.
263
+ *
264
+ * @param {BoundingBox[]} boundingBoxes - Array of bounding box definitions with coordinates and page numbers
265
+ */
266
+ public setBoundingBoxHighlights(boundingBoxes: BoundingBox[]): void {
267
+ this.store.dispatch(store.setBoundingBoxHighlightsAction(boundingBoxes));
268
+ this.render();
269
+ }
270
+
271
+ /**
272
+ * Sets the view mode for the annotator. Annotations and bounding boxes are mutually exclusive.
273
+ * Triggers a re-render.
274
+ *
275
+ * Side effects:
276
+ * - Switching to 'boundingBoxes': Clears active annotation ID
277
+ * - Switching to 'annotations': Clears selected bounding box ID
278
+ *
279
+ * @param {ViewMode} viewMode - Either 'annotations' (default) or 'boundingBoxes'
280
+ */
281
+ public setViewMode(viewMode: ViewMode): void {
282
+ this.store.dispatch(store.setViewModeAction(viewMode));
283
+ this.render();
284
+ }
285
+
286
+ /**
287
+ * Selects a bounding box highlight and scrolls to it. Pass null to deselect.
288
+ *
289
+ * Note: For programmatic navigation (prev/next), use the navigateBoundingBoxHighlightAction
290
+ * which emits BOUNDING_BOX_HIGHLIGHT_NAVIGATE events.
291
+ *
292
+ * @param {string | null} highlightId - The ID of the bounding box to select, or null to clear selection
293
+ */
294
+ public selectBoundingBoxHighlight(highlightId: string | null): void {
295
+ this.store.dispatch(store.setSelectedBoundingBoxHighlightAction(highlightId));
296
+ if (highlightId) {
297
+ this.scrollToBoundingBoxHighlight(highlightId);
298
+ }
299
+ }
300
+
186
301
  public setActiveId(annotationId: string | null): void {
187
302
  this.store.dispatch(store.setActiveAnnotationIdAction(annotationId));
188
303
  }
@@ -235,6 +350,38 @@ export default class BaseAnnotator extends EventEmitter {
235
350
  this.setColor(color);
236
351
  };
237
352
 
353
+ protected handleSetBoundingBoxHighlights = (boundingBoxes: BoundingBox[]): void => {
354
+ this.setBoundingBoxHighlights(boundingBoxes);
355
+ };
356
+
357
+ protected handleNavigateBoundingBoxHighlight = (highlightId: string): void => {
358
+ this.scrollToBoundingBoxHighlight(highlightId);
359
+ };
360
+
361
+ protected handleSelectBoundingBoxHighlight = (highlightId: string | null): void => {
362
+ this.selectBoundingBoxHighlight(highlightId);
363
+ };
364
+
365
+ protected handleSetViewMode = ({ viewMode }: { viewMode: ViewMode }): void => {
366
+ this.setViewMode(viewMode);
367
+ };
368
+
369
+ protected handleSidebarAnnotationUpdate = (annotation: store.SidebarAnnotationUpdatePayload): void => {
370
+ this.store.dispatch(store.applySidebarAnnotationUpdateAction(annotation));
371
+ };
372
+
373
+ protected handleSidebarReplyCreate = (payload: store.SidebarReplyMutationPayload): void => {
374
+ this.store.dispatch(store.applySidebarReplyCreateAction(payload));
375
+ };
376
+
377
+ protected handleSidebarReplyUpdate = (payload: store.SidebarReplyMutationPayload): void => {
378
+ this.store.dispatch(store.applySidebarReplyUpdateAction(payload));
379
+ };
380
+
381
+ protected handleSidebarReplyDelete = ({ annotationId, id }: { annotationId: string; id: string }): void => {
382
+ this.store.dispatch(store.applySidebarReplyDeleteAction({ annotationId, replyId: id }));
383
+ };
384
+
238
385
  protected hydrate(): void {
239
386
  // Redux dispatch method signature doesn't seem to like async actions
240
387
  this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -1,6 +1,7 @@
1
1
  import * as ReactDOM from 'react-dom/client';
2
2
  import { IntlShape } from 'react-intl';
3
3
  import { Store } from 'redux';
4
+ import { AnnotationCallbacks } from './AnnotationCallbacksContext';
4
5
  import { applyResinTags } from '../utils/resin';
5
6
  import { TARGET_TYPE } from '../constants';
6
7
 
@@ -14,6 +15,7 @@ export type Options = {
14
15
  };
15
16
 
16
17
  export type Props = {
18
+ callbacks?: AnnotationCallbacks;
17
19
  intl: IntlShape;
18
20
  store: Store;
19
21
  };
@@ -6,7 +6,9 @@ export default function DeselectListener(): null {
6
6
  const dispatch = useDispatch();
7
7
 
8
8
  React.useEffect(() => {
9
- const handleMouseDown = (): void => {
9
+ const handleMouseDown = (event: MouseEvent): void => {
10
+ // Popup is portaled outside .ba-Layer; native mousedown still bubbles here.
11
+ if ((event.target as Element | null)?.closest?.('.ba-PopupV2, .ba-Popup')) return;
10
12
  dispatch(setActiveAnnotationIdAction(null));
11
13
  };
12
14
 
@@ -2,8 +2,10 @@ import * as React from 'react';
2
2
  import { IntlShape, RawIntlProvider } from 'react-intl';
3
3
  import { Provider as StoreProvider } from 'react-redux';
4
4
  import { Store } from 'redux';
5
+ import AnnotationCallbacksContext, { AnnotationCallbacks } from './AnnotationCallbacksContext';
5
6
 
6
7
  type WrapperProps = {
8
+ callbacks?: AnnotationCallbacks;
7
9
  intl: IntlShape;
8
10
  store: Store;
9
11
  };
@@ -11,11 +13,13 @@ type WrapperProps = {
11
13
  type WrapperReturn<P> = React.FC<P & WrapperProps>;
12
14
 
13
15
  export default function withProviders<P extends object>(WrappedComponent: React.ComponentType<P>): WrapperReturn<P> {
14
- return function RootProvider({ intl, store, ...rest }: P & WrapperProps): JSX.Element {
16
+ return function RootProvider({ callbacks, intl, store, ...rest }: P & WrapperProps): JSX.Element {
15
17
  return (
16
18
  <RawIntlProvider value={intl}>
17
19
  <StoreProvider store={store}>
18
- <WrappedComponent {...(rest as P)} />
20
+ <AnnotationCallbacksContext.Provider value={callbacks ?? {}}>
21
+ <WrappedComponent {...(rest as P)} />
22
+ </AnnotationCallbacksContext.Provider>
19
23
  </StoreProvider>
20
24
  </RawIntlProvider>
21
25
  );
@@ -0,0 +1,8 @@
1
+ .ba-BoundingBoxHighlightList {
2
+ position: absolute;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100%;
6
+ height: 100%;
7
+ pointer-events: none;
8
+ }
@@ -0,0 +1,67 @@
1
+ import * as React from 'react';
2
+ import BoundingBoxHighlightRect from './BoundingBoxHighlightRect';
3
+ import { BoundingBox } from '../../store/boundingBoxHighlights/types';
4
+ import './BoundingBoxHighlightList.scss';
5
+
6
+ type Props = {
7
+ /**
8
+ * All bounding box highlights in the document.
9
+ */
10
+ allBoundingBoxes: BoundingBox[];
11
+ /**
12
+ * Bounding boxes on the current page.
13
+ */
14
+ boundingBoxes: BoundingBox[];
15
+ /**
16
+ * Callback invoked to navigate to the previous or next bounding box highlight.
17
+ */
18
+ onNavigate?: (id: string) => void;
19
+ /**
20
+ * Callback invoked when the user clicks on a bounding box highlight.
21
+ */
22
+ onSelect?: (id: string) => void;
23
+ /**
24
+ * The ID of the selected bounding box highlight.
25
+ */
26
+ selectedId: string | null;
27
+ };
28
+
29
+ const BoundingBoxHighlightList = ({
30
+ allBoundingBoxes,
31
+ boundingBoxes,
32
+ onNavigate,
33
+ onSelect,
34
+ selectedId,
35
+ }: Props): React.ReactElement | null => {
36
+ const total = allBoundingBoxes.length;
37
+
38
+ if (total === 0) {
39
+ return null;
40
+ }
41
+
42
+ const selectedIndex = allBoundingBoxes.findIndex(h => h.id === selectedId);
43
+ const prevId = selectedIndex > 0 ? allBoundingBoxes[selectedIndex - 1].id : undefined;
44
+ const nextId = selectedIndex < total - 1 ? allBoundingBoxes[selectedIndex + 1].id : undefined;
45
+
46
+ return (
47
+ <div className="ba-BoundingBoxHighlightList" data-testid="ba-BoundingBoxHighlightList">
48
+ {boundingBoxes.map(bbox => {
49
+ return (
50
+ <BoundingBoxHighlightRect
51
+ key={bbox.id}
52
+ boundingBox={bbox}
53
+ currentIndex={selectedIndex}
54
+ isSelected={selectedId === bbox.id}
55
+ nextId={nextId}
56
+ onNavigate={onNavigate}
57
+ onSelect={onSelect}
58
+ prevId={prevId}
59
+ total={total}
60
+ />
61
+ );
62
+ })}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default React.memo(BoundingBoxHighlightList);
@@ -0,0 +1,73 @@
1
+ $ba-BoundingBoxHighlightNav-bg: #fff;
2
+ $ba-BoundingBoxHighlightNav-shadow: 0 6px 20px rgba(0, 0, 0, .1);
3
+ $ba-BoundingBoxHighlightNav-arrow: #909090;
4
+ $ba-BoundingBoxHighlightNav-border: #e8e8e8;
5
+ $ba-BoundingBoxHighlightNav-counter: #222;
6
+
7
+ .ba-BoundingBoxHighlightNav {
8
+ position: absolute;
9
+ bottom: 100%;
10
+ left: 50%;
11
+ z-index: 1;
12
+ display: flex;
13
+ gap: 4px;
14
+ align-items: center;
15
+ margin-bottom: 6px;
16
+ padding: 3px;
17
+ white-space: nowrap;
18
+ background: $ba-BoundingBoxHighlightNav-bg;
19
+ border: 1px solid $ba-BoundingBoxHighlightNav-border;
20
+ border-radius: 20px;
21
+ box-shadow: $ba-BoundingBoxHighlightNav-shadow;
22
+ transform: translateX(-50%);
23
+ cursor: default;
24
+ pointer-events: auto;
25
+ }
26
+
27
+ .ba-BoundingBoxHighlightNav-btn {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ width: 32px;
32
+ height: 32px;
33
+ padding: 0;
34
+ line-height: 1;
35
+ background: transparent;
36
+ border: none;
37
+ border-radius: 50%;
38
+ cursor: pointer;
39
+ transition: background-color 150ms ease;
40
+
41
+ svg {
42
+ path {
43
+ fill: $ba-BoundingBoxHighlightNav-arrow;
44
+ }
45
+ }
46
+
47
+ &:hover:not(:disabled) {
48
+ background: rgba(0, 0, 0, .06);
49
+ }
50
+
51
+ &:active:not(:disabled) {
52
+ background: rgba(0, 0, 0, .12);
53
+ }
54
+
55
+ &:disabled {
56
+ cursor: default;
57
+
58
+ svg {
59
+ path {
60
+ fill: $ba-BoundingBoxHighlightNav-border;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ .ba-BoundingBoxHighlightNav-counter {
67
+ padding: 0 4px;
68
+ color: $ba-BoundingBoxHighlightNav-counter;
69
+ font-weight: normal;
70
+ font-size: 15px;
71
+ font-family: Lato, Arial, sans-serif;
72
+ user-select: none;
73
+ }
@@ -0,0 +1,68 @@
1
+ import * as React from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import ChevronLeft from '../../icons/ChevronLeft';
4
+ import ChevronRight from '../../icons/ChevronRight';
5
+ import messages from './messages';
6
+ import './BoundingBoxHighlightNav.scss';
7
+
8
+ type Props = {
9
+ currentIndex: number;
10
+ total: number;
11
+ onPrev: (event: React.MouseEvent | KeyboardEvent) => void;
12
+ onNext: (event: React.MouseEvent | KeyboardEvent) => void;
13
+ };
14
+
15
+ const BoundingBoxHighlightNav = ({ currentIndex, total, onPrev, onNext }: Props): JSX.Element => {
16
+ const intl = useIntl();
17
+ const isPrevDisabled = currentIndex === 0;
18
+ const isNextDisabled = currentIndex === total - 1;
19
+
20
+ React.useEffect(() => {
21
+ const handleKeyDown = (event: KeyboardEvent): void => {
22
+ if (event.key === 'ArrowLeft') {
23
+ event.stopPropagation();
24
+ if (!isPrevDisabled) {
25
+ onPrev(event);
26
+ }
27
+ } else if (event.key === 'ArrowRight') {
28
+ event.stopPropagation();
29
+ if (!isNextDisabled) {
30
+ onNext(event);
31
+ }
32
+ }
33
+ };
34
+
35
+ document.addEventListener('keydown', handleKeyDown, { capture: true });
36
+ return () => document.removeEventListener('keydown', handleKeyDown, { capture: true });
37
+ }, [isPrevDisabled, isNextDisabled, onPrev, onNext]);
38
+
39
+ return (
40
+ <div className="ba-BoundingBoxHighlightNav" data-testid="ba-BoundingBoxHighlightNav">
41
+ <button
42
+ aria-label={intl.formatMessage(messages.ariaLabelViewPrevReference)}
43
+ className="ba-BoundingBoxHighlightNav-btn"
44
+ data-testid="ba-BoundingBoxHighlightNav-prev"
45
+ disabled={isPrevDisabled}
46
+ onClick={onPrev}
47
+ type="button"
48
+ >
49
+ <ChevronLeft width={20} height={20} />
50
+ </button>
51
+ <span className="ba-BoundingBoxHighlightNav-counter">
52
+ {currentIndex + 1} / {total}
53
+ </span>
54
+ <button
55
+ aria-label={intl.formatMessage(messages.ariaLabelViewNextReference)}
56
+ className="ba-BoundingBoxHighlightNav-btn"
57
+ data-testid="ba-BoundingBoxHighlightNav-next"
58
+ disabled={isNextDisabled}
59
+ onClick={onNext}
60
+ type="button"
61
+ >
62
+ <ChevronRight width={20} height={20} />
63
+ </button>
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export default BoundingBoxHighlightNav;
@@ -0,0 +1,22 @@
1
+ $ba-BoundingBoxHighlight-color: #0061d5;
2
+ $ba-BoundingBoxHighlight-bgColor--default: rgba($ba-BoundingBoxHighlight-color, .05);
3
+ $ba-BoundingBoxHighlight-bgColor--hover: rgba($ba-BoundingBoxHighlight-color, .16);
4
+ $ba-BoundingBoxHighlight-bgColor--selected: rgba($ba-BoundingBoxHighlight-color, .1);
5
+
6
+ .ba-BoundingBoxHighlightRect {
7
+ position: absolute;
8
+ box-sizing: border-box;
9
+ background-color: $ba-BoundingBoxHighlight-bgColor--default;
10
+ border: 2px solid rgba($ba-BoundingBoxHighlight-color, .2);
11
+ border-radius: 8px;
12
+ outline: none;
13
+ cursor: pointer;
14
+ transition: background-color 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
15
+ pointer-events: auto;
16
+
17
+ &.is-selected {
18
+ background-color: $ba-BoundingBoxHighlight-bgColor--selected;
19
+ border-color: $ba-BoundingBoxHighlight-color;
20
+ box-shadow: 0 0 0 2px rgba($ba-BoundingBoxHighlight-color, .3), 0 4px 12px rgba(0, 0, 0, .15);
21
+ }
22
+ }