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.
- package/dist/annotations.css +3 -4
- package/dist/annotations.js +1 -1
- package/dist/annotations.js.LICENSE.txt +3 -43
- package/i18n/en-US.js +4 -0
- package/i18n/en-US.properties +8 -0
- package/i18n/json/src/components/BoundingBoxHighlight/messages.json +1 -0
- package/i18n/json/src/components/Popups/messages.json +1 -1
- package/index.js +1 -1
- package/package.json +33 -15
- package/src/@types/api.ts +3 -2
- package/src/@types/events.ts +28 -7
- package/src/@types/global.ts +0 -9
- package/src/@types/model.ts +14 -0
- package/src/BoxAnnotations.ts +4 -1
- package/src/adapters/threadedAnnotationsAdapters.ts +164 -0
- package/src/api/APIFactory.ts +6 -1
- package/src/api/types.ts +50 -1
- package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +33 -0
- package/src/boundingBoxHighlight/BoundingBoxHighlightManager.tsx +19 -0
- package/src/boundingBoxHighlight/index.ts +1 -0
- package/src/common/AnnotationCallbacksContext.ts +7 -0
- package/src/common/BaseAnnotator.scss +14 -1
- package/src/common/BaseAnnotator.ts +161 -14
- package/src/common/BaseManager.ts +2 -0
- package/src/common/DeselectListener.tsx +3 -1
- package/src/common/withProviders.tsx +6 -2
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.scss +8 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightList.tsx +67 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +73 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.tsx +68 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +22 -0
- package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.tsx +77 -0
- package/src/components/BoundingBoxHighlight/index.ts +4 -0
- package/src/components/BoundingBoxHighlight/messages.ts +14 -0
- package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
- package/src/components/Popups/PopupReply.tsx +8 -28
- package/src/components/Popups/PopupV2.scss +28 -0
- package/src/components/Popups/PopupV2.tsx +382 -0
- package/src/components/Popups/messages.ts +10 -0
- package/src/document/DocumentAnnotator.ts +79 -10
- package/src/document/docUtil.ts +2 -1
- package/src/drawing/DrawingAnnotations.tsx +23 -14
- package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
- package/src/drawing/DrawingCreator.tsx +9 -16
- package/src/drawing/DrawingManager.tsx +13 -2
- package/src/drawing/DrawingTarget.tsx +1 -0
- package/src/highlight/HighlightAnnotations.tsx +27 -14
- package/src/highlight/HighlightContainer.tsx +3 -0
- package/src/highlight/HighlightCreatorManager.ts +8 -5
- package/src/highlight/HighlightListener.ts +4 -2
- package/src/highlight/HighlightManager.tsx +13 -2
- package/src/highlight/HighlightTarget.tsx +1 -0
- package/src/icons/ChevronLeft.tsx +24 -0
- package/src/icons/ChevronRight.tsx +24 -0
- package/src/image/ImageAnnotator.ts +50 -3
- package/src/media/MediaAnnotator.ts +2 -1
- package/src/popup/PopupContainer.tsx +11 -1
- package/src/popup/PopupLayer.tsx +97 -22
- package/src/popup/PopupManager.tsx +24 -4
- package/src/region/RegionAnnotation.tsx +1 -0
- package/src/region/RegionCreation.tsx +4 -7
- package/src/region/RegionCreationContainer.tsx +2 -2
- package/src/region/RegionCreator.tsx +9 -17
- package/src/store/annotations/actions.ts +167 -3
- package/src/store/annotations/reducer.ts +71 -1
- package/src/store/boundingBoxHighlights/actions.ts +6 -0
- package/src/store/boundingBoxHighlights/index.ts +4 -0
- package/src/store/boundingBoxHighlights/reducer.ts +33 -0
- package/src/store/boundingBoxHighlights/selectors.ts +12 -0
- package/src/store/boundingBoxHighlights/types.ts +13 -0
- package/src/store/createRootReducer.ts +2 -0
- package/src/store/eventing/boundingBoxHighlightNav.ts +13 -0
- package/src/store/eventing/create.ts +3 -3
- package/src/store/eventing/delete.ts +27 -0
- package/src/store/eventing/middleware.ts +42 -3
- package/src/store/eventing/replyCreate.ts +34 -0
- package/src/store/eventing/replyDelete.ts +35 -0
- package/src/store/eventing/replyUpdate.ts +39 -0
- package/src/store/eventing/types.ts +4 -4
- package/src/store/eventing/update.ts +29 -0
- package/src/store/highlight/actions.ts +32 -3
- package/src/store/highlight/types.ts +1 -0
- package/src/store/index.ts +1 -0
- package/src/store/options/actions.ts +2 -0
- package/src/store/options/reducer.ts +9 -2
- package/src/store/options/selectors.ts +5 -1
- package/src/store/options/types.ts +7 -1
- package/src/store/types.ts +2 -0
- package/src/utils/rotate.ts +43 -0
- package/src/utils/scroll.ts +8 -3
- 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';
|
|
@@ -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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
<
|
|
20
|
+
<AnnotationCallbacksContext.Provider value={callbacks ?? {}}>
|
|
21
|
+
<WrappedComponent {...(rest as P)} />
|
|
22
|
+
</AnnotationCallbacksContext.Provider>
|
|
19
23
|
</StoreProvider>
|
|
20
24
|
</RawIntlProvider>
|
|
21
25
|
);
|
|
@@ -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
|
+
}
|