box-annotations 5.2.1-beta.1 → 5.2.1-beta.12
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/package.json +28 -15
- package/src/@types/events.ts +4 -0
- package/src/@types/global.ts +0 -9
- package/src/@types/model.ts +5 -0
- package/src/BoxAnnotations.ts +3 -1
- package/src/adapters/threadedAnnotationsAdapters.ts +155 -0
- package/src/api/types.ts +28 -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/BaseAnnotator.scss +13 -1
- package/src/common/BaseAnnotator.ts +129 -0
- package/src/common/DeselectListener.tsx +3 -1
- 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 +322 -0
- package/src/components/Popups/messages.ts +10 -0
- package/src/document/DocumentAnnotator.ts +77 -9
- package/src/drawing/DrawingAnnotations.tsx +22 -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/HighlightTarget.tsx +1 -0
- package/src/icons/ChevronLeft.tsx +24 -0
- package/src/icons/ChevronRight.tsx +24 -0
- package/src/image/ImageAnnotator.ts +49 -3
- package/src/media/MediaAnnotator.ts +1 -1
- package/src/popup/PopupContainer.tsx +11 -1
- package/src/popup/PopupLayer.tsx +69 -22
- package/src/popup/PopupManager.tsx +21 -3
- 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 +78 -3
- package/src/store/annotations/reducer.ts +23 -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/middleware.ts +3 -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 +4 -0
- package/src/store/options/types.ts +6 -0
- 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
|
@@ -7,7 +7,10 @@ import EventEmitter from './EventEmitter';
|
|
|
7
7
|
import i18n from '../utils/i18n';
|
|
8
8
|
import messages from '../messages';
|
|
9
9
|
import { Event, IntlOptions, LegacyEvent, Permissions } 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,6 +40,8 @@ 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
|
token: string;
|
|
@@ -44,6 +49,7 @@ export type Options = {
|
|
|
44
49
|
|
|
45
50
|
export const CSS_CONTAINER_CLASS = 'ba';
|
|
46
51
|
export const CSS_LOADED_CLASS = 'ba-annotations-loaded';
|
|
52
|
+
export const CSS_POPUP_PORTAL_CLASS = 'ba-PopupPortal';
|
|
47
53
|
export const ANNOTATION_CLASSES: { [M in store.Mode]?: string } = {
|
|
48
54
|
[store.Mode.HIGHLIGHT]: 'ba-is-create--highlight',
|
|
49
55
|
[store.Mode.REGION]: 'ba-is-create--region',
|
|
@@ -54,6 +60,8 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
54
60
|
|
|
55
61
|
containerEl?: HTMLElement | null;
|
|
56
62
|
|
|
63
|
+
popupPortalEl?: HTMLElement | null;
|
|
64
|
+
|
|
57
65
|
container: Container;
|
|
58
66
|
|
|
59
67
|
deselectManager: DeselectManager | null = null;
|
|
@@ -72,6 +80,7 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
72
80
|
fileOptions,
|
|
73
81
|
initialColor,
|
|
74
82
|
initialMode,
|
|
83
|
+
initialViewMode = 'annotations',
|
|
75
84
|
intl,
|
|
76
85
|
token,
|
|
77
86
|
}: Options) {
|
|
@@ -88,11 +97,14 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
88
97
|
},
|
|
89
98
|
common: { color: initialColor, mode: initialMode },
|
|
90
99
|
options: {
|
|
100
|
+
apiHost,
|
|
91
101
|
features,
|
|
92
102
|
fileId: file.id,
|
|
93
103
|
fileVersionId: fileOptionsVersionId ?? fileVersionId,
|
|
94
104
|
isCurrentFileVersion: !fileOptionsVersionId || fileOptionsVersionId === currentFileVersionId,
|
|
95
105
|
permissions: file.permissions,
|
|
106
|
+
token,
|
|
107
|
+
viewMode: initialViewMode,
|
|
96
108
|
},
|
|
97
109
|
};
|
|
98
110
|
|
|
@@ -109,6 +121,10 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
109
121
|
this.addListener(Event.ANNOTATION_REMOVE, this.handleRemove);
|
|
110
122
|
this.addListener(Event.COLOR_SET, this.handleSetColor);
|
|
111
123
|
this.addListener(Event.VISIBLE_SET, this.handleSetVisible);
|
|
124
|
+
this.addListener(Event.BOUNDING_BOX_HIGHLIGHTS_SET, this.handleSetBoundingBoxHighlights);
|
|
125
|
+
this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
|
|
126
|
+
this.addListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
|
|
127
|
+
this.addListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
|
|
112
128
|
|
|
113
129
|
// Load any required data at startup
|
|
114
130
|
this.hydrate();
|
|
@@ -119,6 +135,11 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
119
135
|
this.containerEl.classList.remove(CSS_CONTAINER_CLASS);
|
|
120
136
|
}
|
|
121
137
|
|
|
138
|
+
if (this.popupPortalEl) {
|
|
139
|
+
this.popupPortalEl.remove();
|
|
140
|
+
this.popupPortalEl = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
122
143
|
if (this.annotatedEl) {
|
|
123
144
|
this.annotatedEl.classList.remove(CSS_LOADED_CLASS);
|
|
124
145
|
}
|
|
@@ -134,6 +155,10 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
134
155
|
this.removeListener(Event.ANNOTATION_REMOVE, this.handleRemove);
|
|
135
156
|
this.removeListener(Event.COLOR_SET, this.handleSetColor);
|
|
136
157
|
this.removeListener(Event.VISIBLE_SET, this.handleSetVisible);
|
|
158
|
+
this.removeListener(Event.BOUNDING_BOX_HIGHLIGHTS_SET, this.handleSetBoundingBoxHighlights);
|
|
159
|
+
this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_NAVIGATE, this.handleNavigateBoundingBoxHighlight);
|
|
160
|
+
this.removeListener(Event.BOUNDING_BOX_HIGHLIGHT_SELECT, this.handleSelectBoundingBoxHighlight);
|
|
161
|
+
this.removeListener(Event.VIEW_MODE_SET, this.handleSetViewMode);
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
public init(scale = 1, rotation = 0): void {
|
|
@@ -149,6 +174,12 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
149
174
|
this.annotatedEl.classList.add(CSS_LOADED_CLASS);
|
|
150
175
|
this.containerEl.classList.add(CSS_CONTAINER_CLASS);
|
|
151
176
|
|
|
177
|
+
if (!this.popupPortalEl) {
|
|
178
|
+
this.popupPortalEl = this.containerEl.ownerDocument.createElement('div');
|
|
179
|
+
this.popupPortalEl.classList.add(CSS_POPUP_PORTAL_CLASS);
|
|
180
|
+
this.containerEl.appendChild(this.popupPortalEl);
|
|
181
|
+
}
|
|
182
|
+
|
|
152
183
|
// Defer to the child class to render annotations
|
|
153
184
|
this.render();
|
|
154
185
|
|
|
@@ -171,6 +202,88 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
171
202
|
// Called by box-content-preview
|
|
172
203
|
}
|
|
173
204
|
|
|
205
|
+
public scrollToBoundingBoxHighlight(highlightId: string | null): void {
|
|
206
|
+
if (!highlightId || !this.annotatedEl) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const highlights = getBoundingBoxHighlights(this.store.getState());
|
|
211
|
+
const highlight = highlights.find((h: BoundingBox) => h.id === highlightId);
|
|
212
|
+
|
|
213
|
+
if (!highlight) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const referenceEl = this.getScrollReferenceForHighlight(highlight);
|
|
218
|
+
if (!referenceEl) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const offsets = {
|
|
223
|
+
x: highlight.x + highlight.width / 2,
|
|
224
|
+
y: highlight.y + highlight.height / 2,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
scrollToLocation(this.annotatedEl, referenceEl, {
|
|
228
|
+
offsets: this.getAdjustedScrollOffsets(offsets),
|
|
229
|
+
smooth: true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Returns the element to scroll relative to for the given bounding box highlight. */
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
235
|
+
protected getScrollReferenceForHighlight(_highlight: BoundingBox): HTMLElement | null | undefined {
|
|
236
|
+
return undefined; // Must be implemented in child class
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Adjusts scroll offsets before passing to scrollToLocation (e.g. to apply rotation). */
|
|
240
|
+
protected getAdjustedScrollOffsets(offsets: { x: number; y: number }): { x: number; y: number } {
|
|
241
|
+
return offsets;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Sets the bounding box highlights to display. This replaces all existing bounding boxes.
|
|
246
|
+
* Triggers a re-render.
|
|
247
|
+
*
|
|
248
|
+
* Note: Bounding boxes are only visible when viewMode is set to 'boundingBoxes'.
|
|
249
|
+
*
|
|
250
|
+
* @param {BoundingBox[]} boundingBoxes - Array of bounding box definitions with coordinates and page numbers
|
|
251
|
+
*/
|
|
252
|
+
public setBoundingBoxHighlights(boundingBoxes: BoundingBox[]): void {
|
|
253
|
+
this.store.dispatch(store.setBoundingBoxHighlightsAction(boundingBoxes));
|
|
254
|
+
this.render();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Sets the view mode for the annotator. Annotations and bounding boxes are mutually exclusive.
|
|
259
|
+
* Triggers a re-render.
|
|
260
|
+
*
|
|
261
|
+
* Side effects:
|
|
262
|
+
* - Switching to 'boundingBoxes': Clears active annotation ID
|
|
263
|
+
* - Switching to 'annotations': Clears selected bounding box ID
|
|
264
|
+
*
|
|
265
|
+
* @param {ViewMode} viewMode - Either 'annotations' (default) or 'boundingBoxes'
|
|
266
|
+
*/
|
|
267
|
+
public setViewMode(viewMode: ViewMode): void {
|
|
268
|
+
this.store.dispatch(store.setViewModeAction(viewMode));
|
|
269
|
+
this.render();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Selects a bounding box highlight and scrolls to it. Pass null to deselect.
|
|
274
|
+
*
|
|
275
|
+
* Note: For programmatic navigation (prev/next), use the navigateBoundingBoxHighlightAction
|
|
276
|
+
* which emits BOUNDING_BOX_HIGHLIGHT_NAVIGATE events.
|
|
277
|
+
*
|
|
278
|
+
* @param {string | null} highlightId - The ID of the bounding box to select, or null to clear selection
|
|
279
|
+
*/
|
|
280
|
+
public selectBoundingBoxHighlight(highlightId: string | null): void {
|
|
281
|
+
this.store.dispatch(store.setSelectedBoundingBoxHighlightAction(highlightId));
|
|
282
|
+
if (highlightId) {
|
|
283
|
+
this.scrollToBoundingBoxHighlight(highlightId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
174
287
|
public setActiveId(annotationId: string | null): void {
|
|
175
288
|
this.store.dispatch(store.setActiveAnnotationIdAction(annotationId));
|
|
176
289
|
}
|
|
@@ -223,6 +336,22 @@ export default class BaseAnnotator extends EventEmitter {
|
|
|
223
336
|
this.setColor(color);
|
|
224
337
|
};
|
|
225
338
|
|
|
339
|
+
protected handleSetBoundingBoxHighlights = (boundingBoxes: BoundingBox[]): void => {
|
|
340
|
+
this.setBoundingBoxHighlights(boundingBoxes);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
protected handleNavigateBoundingBoxHighlight = (highlightId: string): void => {
|
|
344
|
+
this.scrollToBoundingBoxHighlight(highlightId);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
protected handleSelectBoundingBoxHighlight = (highlightId: string | null): void => {
|
|
348
|
+
this.selectBoundingBoxHighlight(highlightId);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
protected handleSetViewMode = ({ viewMode }: { viewMode: ViewMode }): void => {
|
|
352
|
+
this.setViewMode(viewMode);
|
|
353
|
+
};
|
|
354
|
+
|
|
226
355
|
protected hydrate(): void {
|
|
227
356
|
// Redux dispatch method signature doesn't seem to like async actions
|
|
228
357
|
this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import BoundingBoxHighlightNav from './BoundingBoxHighlightNav';
|
|
4
|
+
import { BoundingBox } from '../../store/boundingBoxHighlights/types';
|
|
5
|
+
import './BoundingBoxHighlightRect.scss';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
boundingBox: BoundingBox;
|
|
9
|
+
currentIndex: number;
|
|
10
|
+
isSelected?: boolean;
|
|
11
|
+
onNavigate?: (id: string) => void;
|
|
12
|
+
onSelect?: (id: string) => void;
|
|
13
|
+
total: number;
|
|
14
|
+
prevId?: string;
|
|
15
|
+
nextId?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const BoundingBoxHighlightRect = ({
|
|
19
|
+
boundingBox,
|
|
20
|
+
currentIndex,
|
|
21
|
+
isSelected,
|
|
22
|
+
onNavigate,
|
|
23
|
+
onSelect,
|
|
24
|
+
total,
|
|
25
|
+
prevId,
|
|
26
|
+
nextId,
|
|
27
|
+
}: Props): JSX.Element => {
|
|
28
|
+
const { id, x, y, width, height } = boundingBox;
|
|
29
|
+
|
|
30
|
+
const style: React.CSSProperties = {
|
|
31
|
+
display: 'block',
|
|
32
|
+
left: `${x}%`,
|
|
33
|
+
top: `${y}%`,
|
|
34
|
+
width: `${width}%`,
|
|
35
|
+
height: `${height}%`,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleClick = (event: React.MouseEvent): void => {
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
onSelect?.(id);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handlePrev = (event: React.MouseEvent | KeyboardEvent): void => {
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
if (prevId) {
|
|
46
|
+
onNavigate?.(prevId);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleNext = (event: React.MouseEvent | KeyboardEvent): void => {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
if (nextId) {
|
|
53
|
+
onNavigate?.(nextId);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={classNames('ba-BoundingBoxHighlightRect', { 'is-selected': isSelected })}
|
|
60
|
+
data-testid={`ba-BoundingBoxHighlightRect-${id}`}
|
|
61
|
+
onClick={handleClick}
|
|
62
|
+
role="presentation"
|
|
63
|
+
style={style}
|
|
64
|
+
>
|
|
65
|
+
{isSelected && total > 1 && (
|
|
66
|
+
<BoundingBoxHighlightNav
|
|
67
|
+
currentIndex={currentIndex}
|
|
68
|
+
onNext={handleNext}
|
|
69
|
+
onPrev={handlePrev}
|
|
70
|
+
total={total}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default BoundingBoxHighlightRect;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as BoundingBoxHighlightList } from './BoundingBoxHighlightList';
|
|
2
|
+
export { default as BoundingBoxHighlightNav } from './BoundingBoxHighlightNav';
|
|
3
|
+
export { default as BoundingBoxHighlightRect } from './BoundingBoxHighlightRect';
|
|
4
|
+
export type { BoundingBox } from '../../store/boundingBoxHighlights/types';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
|
|
3
|
+
export default defineMessages({
|
|
4
|
+
ariaLabelViewNextReference: {
|
|
5
|
+
id: 'ba.boundingBoxHighlight.viewNextReference',
|
|
6
|
+
description: 'Aria label for the button to view the next reference',
|
|
7
|
+
defaultMessage: 'View next reference',
|
|
8
|
+
},
|
|
9
|
+
ariaLabelViewPrevReference: {
|
|
10
|
+
id: 'ba.boundingBoxHighlight.viewPrevReference',
|
|
11
|
+
description: 'Aria label for the button to view the previous reference',
|
|
12
|
+
defaultMessage: 'View previous reference',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -33,12 +33,6 @@ export type Props = {
|
|
|
33
33
|
|
|
34
34
|
const options: Partial<Options> = {
|
|
35
35
|
modifiers: [
|
|
36
|
-
{
|
|
37
|
-
name: 'eventListeners',
|
|
38
|
-
options: {
|
|
39
|
-
scroll: false,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
36
|
{
|
|
43
37
|
name: 'offset',
|
|
44
38
|
options: {
|
|
@@ -48,7 +42,6 @@ const options: Partial<Options> = {
|
|
|
48
42
|
{
|
|
49
43
|
name: 'preventOverflow',
|
|
50
44
|
options: {
|
|
51
|
-
altAxis: true,
|
|
52
45
|
padding: 50,
|
|
53
46
|
},
|
|
54
47
|
},
|
|
@@ -3,21 +3,19 @@ import Popper from '@popperjs/core';
|
|
|
3
3
|
import { useSelector } from 'react-redux';
|
|
4
4
|
import { useIntl } from 'react-intl';
|
|
5
5
|
import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
|
|
6
|
+
|
|
7
|
+
import ReplyForm from '../ReplyForm';
|
|
8
|
+
|
|
9
|
+
import { getRotation, getScale } from '../../store/options';
|
|
10
|
+
|
|
6
11
|
import messages from './messages';
|
|
7
12
|
import PopupBase from './PopupBase';
|
|
8
|
-
import PopupReplyV2 from './PopupReplyV2';
|
|
9
|
-
import ReplyForm from '../ReplyForm';
|
|
10
|
-
import { getScale, getRotation } from '../../store/options';
|
|
11
13
|
import { PopupReference } from './Popper';
|
|
12
14
|
import './PopupReply.scss';
|
|
13
15
|
|
|
14
|
-
type
|
|
16
|
+
export type Props = {
|
|
15
17
|
className?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type LegacyProps = BaseProps & {
|
|
19
18
|
isPending: boolean;
|
|
20
|
-
isThreadedAnnotation?: false;
|
|
21
19
|
onCancel: (text?: string) => void;
|
|
22
20
|
onChange: (text?: string) => void;
|
|
23
21
|
onSubmit: (text: string) => void;
|
|
@@ -25,18 +23,6 @@ type LegacyProps = BaseProps & {
|
|
|
25
23
|
value?: string;
|
|
26
24
|
};
|
|
27
25
|
|
|
28
|
-
type ThreadedProps = BaseProps & {
|
|
29
|
-
isThreadedAnnotation: true;
|
|
30
|
-
isPending?: boolean;
|
|
31
|
-
onCancel?: (text?: string) => void;
|
|
32
|
-
onChange?: (text?: string) => void;
|
|
33
|
-
onSubmit?: (text: string) => void;
|
|
34
|
-
reference?: PopupReference;
|
|
35
|
-
value?: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type Props = LegacyProps | ThreadedProps;
|
|
39
|
-
|
|
40
26
|
const isIE = (): boolean => {
|
|
41
27
|
const { userAgent } = navigator;
|
|
42
28
|
return userAgent.indexOf('Trident/') > 0;
|
|
@@ -97,7 +83,7 @@ const getOptions = (): Partial<Popper.Options> => {
|
|
|
97
83
|
export default function PopupReply(props: Props): JSX.Element {
|
|
98
84
|
const intl = useIntl();
|
|
99
85
|
const popupRef = React.useRef<PopupBase>(null);
|
|
100
|
-
const popupOptions = React.useRef<Partial<Popper.Options>>(getOptions());
|
|
86
|
+
const popupOptions = React.useRef<Partial<Popper.Options>>(getOptions());
|
|
101
87
|
const rotation = useSelector(getRotation);
|
|
102
88
|
const scale = useSelector(getScale);
|
|
103
89
|
|
|
@@ -109,13 +95,7 @@ export default function PopupReply(props: Props): JSX.Element {
|
|
|
109
95
|
}
|
|
110
96
|
}, [popupRef, rotation, scale]);
|
|
111
97
|
|
|
112
|
-
|
|
113
|
-
// are not yet forwarded and will need to be wired up when V2 is implemented.
|
|
114
|
-
if (props.isThreadedAnnotation) {
|
|
115
|
-
return <PopupReplyV2 />;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const { isPending, onCancel, onChange, onSubmit, value = '', isThreadedAnnotation: _, ...rest } = props as LegacyProps;
|
|
98
|
+
const { isPending, onCancel, onChange, onSubmit, value = '', ...rest } = props;
|
|
119
99
|
|
|
120
100
|
return (
|
|
121
101
|
<FocusTrap>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.ba-PopupV2 {
|
|
2
|
+
position: absolute;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
z-index: 150;
|
|
6
|
+
width: 348px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Specificity must beat .ba selectors from box-ui-elements globals
|
|
10
|
+
// (forms, buttons, links) that cascade into threaded-annotations components.
|
|
11
|
+
.ba .ba-PopupV2 {
|
|
12
|
+
div[contenteditable='true'],
|
|
13
|
+
div[contenteditable='true']:hover,
|
|
14
|
+
div[contenteditable='true']:focus {
|
|
15
|
+
width: auto;
|
|
16
|
+
padding: 0;
|
|
17
|
+
color: inherit;
|
|
18
|
+
border: none;
|
|
19
|
+
border-radius: 0;
|
|
20
|
+
outline: none;
|
|
21
|
+
box-shadow: none;
|
|
22
|
+
transition: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button svg {
|
|
26
|
+
pointer-events: auto;
|
|
27
|
+
}
|
|
28
|
+
}
|