box-annotations 5.2.1-beta.1 → 5.2.1-beta.11
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 +2 -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/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 +76 -8
- package/src/drawing/DrawingAnnotations.tsx +3 -0
- package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
- package/src/drawing/DrawingCreator.tsx +9 -16
- 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 +48 -2
- package/src/media/MediaAnnotator.ts +1 -1
- package/src/popup/PopupContainer.tsx +11 -1
- package/src/popup/PopupLayer.tsx +66 -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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import noop from 'lodash/noop';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom';
|
|
4
|
+
import { useIntl } from 'react-intl';
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
|
|
7
|
+
import { BlueprintModernizationProvider, TooltipProvider } from '@box/blueprint-web';
|
|
8
|
+
import {
|
|
9
|
+
MentionContextProvider,
|
|
10
|
+
MessageEditorV2,
|
|
11
|
+
ThreadedAnnotationsV2,
|
|
12
|
+
serializeMentionMarkup,
|
|
13
|
+
} from '@box/threaded-annotations';
|
|
14
|
+
import type { DocumentNodeV2, TextMessageTypeV2 } from '@box/threaded-annotations';
|
|
15
|
+
import type { FetchedAvatarUrls, UserContactType } from '@box/user-selector';
|
|
16
|
+
import type { JSONContent } from '@tiptap/core';
|
|
17
|
+
import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
|
|
18
|
+
|
|
19
|
+
import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
|
|
20
|
+
import {
|
|
21
|
+
createReplyAction,
|
|
22
|
+
deleteAnnotationAction,
|
|
23
|
+
setActiveAnnotationIdAction,
|
|
24
|
+
updateAnnotationAction,
|
|
25
|
+
} from '../../store/annotations/actions';
|
|
26
|
+
import { getAnnotation } from '../../store/annotations/selectors';
|
|
27
|
+
import { getApiHost, getToken } from '../../store/options';
|
|
28
|
+
import { fetchCollaboratorsAction } from '../../store/users/actions';
|
|
29
|
+
|
|
30
|
+
import type { AppState, AppThunkDispatch } from '../../store/types';
|
|
31
|
+
|
|
32
|
+
import createPopper, { PopupReference } from './Popper';
|
|
33
|
+
import type { Instance, Options } from './Popper';
|
|
34
|
+
import messages from './messages';
|
|
35
|
+
import './PopupV2.scss';
|
|
36
|
+
|
|
37
|
+
export type Props = {
|
|
38
|
+
annotationId?: string;
|
|
39
|
+
onSubmit: (text: string) => void;
|
|
40
|
+
popupPortalEl?: HTMLElement | null;
|
|
41
|
+
reference: PopupReference;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getPopupOptions = (): Partial<Options> => ({
|
|
45
|
+
modifiers: [
|
|
46
|
+
{ name: 'flip', options: { altAxis: false, fallbackPlacements: ['top', 'left', 'right'] } },
|
|
47
|
+
{ name: 'offset', options: { offset: [0, 15] } },
|
|
48
|
+
{ name: 'preventOverflow', options: { padding: 5 } },
|
|
49
|
+
],
|
|
50
|
+
placement: 'bottom',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const createDocumentNode = (content: JSONContent | null): DocumentNodeV2 => {
|
|
54
|
+
if (!content) {
|
|
55
|
+
return { type: 'doc', content: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (content.type === 'doc' && content.content) {
|
|
59
|
+
return content as DocumentNodeV2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { type: 'doc', content: [content] } as DocumentNodeV2;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Callers render initials as a fallback on null.
|
|
66
|
+
// A persistent null across all users usually indicates a stale token.
|
|
67
|
+
const fetchAvatarBlob = async (apiHost: string, token: string, userId: string): Promise<string | null> => {
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(`${apiHost}/2.0/users/${userId}/avatar?pic_type=large`, {
|
|
70
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) return null;
|
|
73
|
+
const blob = await response.blob();
|
|
74
|
+
return URL.createObjectURL(blob);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
|
|
81
|
+
const intl = useIntl();
|
|
82
|
+
const dispatch = useDispatch<AppThunkDispatch>();
|
|
83
|
+
const popupRef = React.useRef<HTMLDivElement>(null);
|
|
84
|
+
const popperRef = React.useRef<Instance>();
|
|
85
|
+
const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
|
|
86
|
+
|
|
87
|
+
const apiHost = useSelector(getApiHost);
|
|
88
|
+
const token = useSelector(getToken);
|
|
89
|
+
const annotation = useSelector((state: AppState) =>
|
|
90
|
+
annotationId ? getAnnotation(state, annotationId) : undefined,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const [avatarBlobs, setAvatarBlobs] = React.useState<Record<string, string>>({});
|
|
94
|
+
const avatarCacheRef = React.useRef<Map<string, string>>(new Map());
|
|
95
|
+
const credentialsRef = React.useRef({ apiHost, token });
|
|
96
|
+
credentialsRef.current = { apiHost, token };
|
|
97
|
+
|
|
98
|
+
const getOrFetchAvatarBlob = React.useCallback(
|
|
99
|
+
async (userId: string): Promise<string | null> => {
|
|
100
|
+
const cached = avatarCacheRef.current.get(userId);
|
|
101
|
+
if (cached) return cached;
|
|
102
|
+
const capturedApiHost = apiHost;
|
|
103
|
+
const capturedToken = token;
|
|
104
|
+
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, userId);
|
|
105
|
+
if (!url) return null;
|
|
106
|
+
if (
|
|
107
|
+
credentialsRef.current.apiHost !== capturedApiHost ||
|
|
108
|
+
credentialsRef.current.token !== capturedToken
|
|
109
|
+
) {
|
|
110
|
+
URL.revokeObjectURL(url);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const existing = avatarCacheRef.current.get(userId);
|
|
114
|
+
if (existing) {
|
|
115
|
+
URL.revokeObjectURL(url);
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
avatarCacheRef.current.set(userId, url);
|
|
119
|
+
return url;
|
|
120
|
+
},
|
|
121
|
+
[apiHost, token],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (popupRef.current) {
|
|
126
|
+
popperRef.current = createPopper(reference, popupRef.current, optionsRef.current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
popperRef.current?.destroy();
|
|
131
|
+
};
|
|
132
|
+
}, [reference]);
|
|
133
|
+
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
const cache = avatarCacheRef.current;
|
|
136
|
+
return () => {
|
|
137
|
+
cache.forEach(url => URL.revokeObjectURL(url));
|
|
138
|
+
cache.clear();
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
React.useEffect(() => {
|
|
143
|
+
avatarCacheRef.current.forEach(url => URL.revokeObjectURL(url));
|
|
144
|
+
avatarCacheRef.current.clear();
|
|
145
|
+
setAvatarBlobs({});
|
|
146
|
+
}, [apiHost, token]);
|
|
147
|
+
|
|
148
|
+
const handleEvent = React.useCallback((event: React.SyntheticEvent) => {
|
|
149
|
+
event.stopPropagation();
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
React.useEffect(() => {
|
|
153
|
+
if (!annotation) return undefined;
|
|
154
|
+
|
|
155
|
+
const userIds = Array.from(
|
|
156
|
+
new Set(annotationToMessages(annotation).map(msg => String(msg.author.id))),
|
|
157
|
+
);
|
|
158
|
+
let cancelled = false;
|
|
159
|
+
|
|
160
|
+
Promise.all(userIds.map(async id => [id, await getOrFetchAvatarBlob(id)] as const)).then(entries => {
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
setAvatarBlobs(prev => {
|
|
163
|
+
const next = { ...prev };
|
|
164
|
+
entries.forEach(([id, url]) => {
|
|
165
|
+
if (url) next[id] = url;
|
|
166
|
+
});
|
|
167
|
+
return next;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
cancelled = true;
|
|
173
|
+
};
|
|
174
|
+
}, [annotation, getOrFetchAvatarBlob]);
|
|
175
|
+
|
|
176
|
+
const threadMessages: TextMessageTypeV2[] = React.useMemo(() => {
|
|
177
|
+
if (!annotation) return [];
|
|
178
|
+
return annotationToMessages(annotation).map(msg => ({
|
|
179
|
+
...msg,
|
|
180
|
+
author: {
|
|
181
|
+
...msg.author,
|
|
182
|
+
avatarUrl: avatarBlobs[String(msg.author.id)],
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
}, [annotation, avatarBlobs]);
|
|
186
|
+
|
|
187
|
+
const isResolved = annotation?.status === 'resolved';
|
|
188
|
+
const resolvedBy = isResolved
|
|
189
|
+
? annotation?.resolution?.resolved_by?.name ?? annotation?.modified_by?.name
|
|
190
|
+
: undefined;
|
|
191
|
+
const resolvedAtSource = isResolved
|
|
192
|
+
? annotation?.resolution?.resolved_at ?? annotation?.modified_at
|
|
193
|
+
: undefined;
|
|
194
|
+
const resolvedAt = resolvedAtSource ? new Date(resolvedAtSource).getTime() : undefined;
|
|
195
|
+
|
|
196
|
+
const userSelectorProps = React.useMemo(
|
|
197
|
+
() => ({
|
|
198
|
+
allowEmptyQuery: true,
|
|
199
|
+
ariaRoleDescription: intl.formatMessage(messages.ariaLabelMentionSelector),
|
|
200
|
+
fetchAvatarUrls: async (userContacts: UserContactType[]): Promise<FetchedAvatarUrls> => {
|
|
201
|
+
const urls: FetchedAvatarUrls = {};
|
|
202
|
+
await Promise.all(
|
|
203
|
+
userContacts.map(async ({ id }) => {
|
|
204
|
+
const key = String(id);
|
|
205
|
+
const blobUrl = await getOrFetchAvatarBlob(key);
|
|
206
|
+
if (blobUrl) {
|
|
207
|
+
urls[key] = blobUrl;
|
|
208
|
+
}
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
return urls;
|
|
212
|
+
},
|
|
213
|
+
fetchUsers: async (query: string) => {
|
|
214
|
+
const action = await dispatch(fetchCollaboratorsAction(query));
|
|
215
|
+
if (fetchCollaboratorsAction.fulfilled.match(action)) {
|
|
216
|
+
return action.payload.entries.map(collaboratorToUserContact);
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
219
|
+
},
|
|
220
|
+
loadingAriaLabel: intl.formatMessage(messages.ariaLabelMentionLoading),
|
|
221
|
+
}),
|
|
222
|
+
[dispatch, getOrFetchAvatarBlob, intl],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const handlePost = React.useCallback(
|
|
226
|
+
async (content: JSONContent | null): Promise<void> => {
|
|
227
|
+
const doc = createDocumentNode(content);
|
|
228
|
+
const { text } = serializeMentionMarkup(doc);
|
|
229
|
+
if (annotationId) {
|
|
230
|
+
await dispatch(createReplyAction({ annotationId, message: text }));
|
|
231
|
+
} else {
|
|
232
|
+
onSubmit(text);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
[annotationId, dispatch, onSubmit],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const handleThreadDelete = React.useCallback(
|
|
239
|
+
async (): Promise<void> => {
|
|
240
|
+
if (annotationId) {
|
|
241
|
+
await dispatch(deleteAnnotationAction(annotationId));
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
[annotationId, dispatch],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const handleResolve = React.useCallback(
|
|
248
|
+
async (): Promise<void> => {
|
|
249
|
+
if (!annotationId) return;
|
|
250
|
+
const result = await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'resolved' } }));
|
|
251
|
+
if (updateAnnotationAction.fulfilled.match(result)) {
|
|
252
|
+
dispatch(setActiveAnnotationIdAction(null));
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[annotationId, dispatch],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const handleUnresolve = React.useCallback(
|
|
259
|
+
async (): Promise<void> => {
|
|
260
|
+
if (!annotationId) return;
|
|
261
|
+
await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'open' } }));
|
|
262
|
+
},
|
|
263
|
+
[annotationId, dispatch],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!popupPortalEl) return null;
|
|
267
|
+
|
|
268
|
+
return ReactDOM.createPortal(
|
|
269
|
+
<FocusTrap>
|
|
270
|
+
<div
|
|
271
|
+
ref={popupRef}
|
|
272
|
+
aria-label={intl.formatMessage(messages.ariaLabelComment)}
|
|
273
|
+
className="ba-PopupV2"
|
|
274
|
+
data-resin-component={annotationId ? 'popupThreadV2' : 'popupReplyV2'}
|
|
275
|
+
onClick={handleEvent}
|
|
276
|
+
onMouseDown={handleEvent}
|
|
277
|
+
onMouseMove={handleEvent}
|
|
278
|
+
onMouseUp={handleEvent}
|
|
279
|
+
onTouchMove={handleEvent}
|
|
280
|
+
onTouchStart={handleEvent}
|
|
281
|
+
role="presentation"
|
|
282
|
+
>
|
|
283
|
+
<BlueprintModernizationProvider enableModernizedComponents>
|
|
284
|
+
<TooltipProvider>
|
|
285
|
+
<MentionContextProvider value={{
|
|
286
|
+
collaborationPopoverProps: {
|
|
287
|
+
onSubmit: () => Promise.resolve(),
|
|
288
|
+
},
|
|
289
|
+
}}>
|
|
290
|
+
{annotationId ? (
|
|
291
|
+
<ThreadedAnnotationsV2
|
|
292
|
+
isAnnotations
|
|
293
|
+
isResolved={isResolved}
|
|
294
|
+
messages={threadMessages}
|
|
295
|
+
onAvatarClick={noop}
|
|
296
|
+
onDelete={noop}
|
|
297
|
+
onPost={handlePost}
|
|
298
|
+
onResolve={handleResolve}
|
|
299
|
+
onThreadDelete={handleThreadDelete}
|
|
300
|
+
onUnresolve={handleUnresolve}
|
|
301
|
+
resolvedAt={resolvedAt}
|
|
302
|
+
resolvedBy={resolvedBy}
|
|
303
|
+
userSelectorProps={userSelectorProps}
|
|
304
|
+
/>
|
|
305
|
+
) : (
|
|
306
|
+
<MessageEditorV2
|
|
307
|
+
isFirstAnnotation
|
|
308
|
+
onPost={handlePost}
|
|
309
|
+
userSelectorProps={userSelectorProps}
|
|
310
|
+
/>
|
|
311
|
+
)}
|
|
312
|
+
</MentionContextProvider>
|
|
313
|
+
</TooltipProvider>
|
|
314
|
+
</BlueprintModernizationProvider>
|
|
315
|
+
<div data-threaded-annotations-portal />
|
|
316
|
+
</div>
|
|
317
|
+
</FocusTrap>,
|
|
318
|
+
popupPortalEl,
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export default PopupV2;
|
|
@@ -6,6 +6,16 @@ export default defineMessages({
|
|
|
6
6
|
description: 'Aria label description for reply popup',
|
|
7
7
|
defaultMessage: 'Comment',
|
|
8
8
|
},
|
|
9
|
+
ariaLabelMentionLoading: {
|
|
10
|
+
id: 'ba.popup.reply.mentionLoading',
|
|
11
|
+
description: 'Aria label for mention selector loading state',
|
|
12
|
+
defaultMessage: 'Loading users...',
|
|
13
|
+
},
|
|
14
|
+
ariaLabelMentionSelector: {
|
|
15
|
+
id: 'ba.popup.reply.mentionSelector',
|
|
16
|
+
description: 'Aria role description for mention user selector',
|
|
17
|
+
defaultMessage: 'Mention a collaborator',
|
|
18
|
+
},
|
|
9
19
|
ariaLabelReplyField: {
|
|
10
20
|
id: 'ba.popup.reply.field',
|
|
11
21
|
description: 'Aria label description for annotation comment field',
|
|
@@ -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,7 +96,13 @@ 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
|
-
|
|
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 }));
|
|
81
106
|
managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
82
107
|
|
|
83
108
|
const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
|
|
@@ -94,7 +119,6 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
97
|
-
|
|
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,50 @@ 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({
|
|
163
227
|
intl: this.intl,
|
|
164
228
|
store: this.store,
|
|
165
|
-
})
|
|
166
|
-
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
167
231
|
|
|
168
232
|
this.managers.set(pageNumber, pageManagers);
|
|
169
233
|
}
|
|
@@ -194,4 +258,8 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
194
258
|
scrollToLocation(this.annotatedEl, annotationPageEl, { offsets });
|
|
195
259
|
}
|
|
196
260
|
}
|
|
261
|
+
|
|
262
|
+
protected getScrollReferenceForHighlight(highlight: BoundingBox): HTMLElement | undefined {
|
|
263
|
+
return this.getPage(highlight.pageNumber);
|
|
264
|
+
}
|
|
197
265
|
}
|
|
@@ -24,6 +24,7 @@ export type Props = {
|
|
|
24
24
|
location: number;
|
|
25
25
|
redoDrawingPathGroup: () => void;
|
|
26
26
|
resetDrawing: () => void;
|
|
27
|
+
rotation: number;
|
|
27
28
|
setActiveAnnotationId: (annotationId: string | null) => void;
|
|
28
29
|
setReferenceId: (uuid: string) => void;
|
|
29
30
|
setStaged: (staged: CreatorItemDrawing | null) => void;
|
|
@@ -47,6 +48,7 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
47
48
|
location,
|
|
48
49
|
redoDrawingPathGroup,
|
|
49
50
|
resetDrawing,
|
|
51
|
+
rotation,
|
|
50
52
|
setActiveAnnotationId,
|
|
51
53
|
setReferenceId,
|
|
52
54
|
setStaged,
|
|
@@ -170,6 +172,7 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
170
172
|
color={color}
|
|
171
173
|
onStart={handleStart}
|
|
172
174
|
onStop={handleStop}
|
|
175
|
+
rotation={rotation}
|
|
173
176
|
/>
|
|
174
177
|
)}
|
|
175
178
|
|
|
@@ -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
|
-
|
|
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] =
|
|
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] =
|
|
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
|
|
@@ -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}`}
|
|
@@ -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;
|