box-annotations 5.2.1-beta.7 → 5.3.0
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 +1 -1
- package/dist/annotations.js +1 -1
- package/dist/annotations.js.LICENSE.txt +1 -1
- package/index.js +1 -1
- package/package.json +33 -22
- package/src/@types/api.ts +3 -2
- package/src/@types/events.ts +27 -10
- package/src/@types/global.ts +0 -9
- package/src/@types/model.ts +9 -0
- package/src/BoxAnnotations.ts +5 -1
- package/src/adapters/threadedAnnotationsAdapters.ts +39 -26
- package/src/api/APIFactory.ts +6 -1
- package/src/api/types.ts +23 -1
- package/src/common/AnnotationCallbacksContext.ts +7 -0
- package/src/common/BaseAnnotator.scss +14 -1
- package/src/common/BaseAnnotator.ts +46 -2
- package/src/common/BaseManager.ts +2 -0
- package/src/common/DeselectListener.tsx +3 -1
- package/src/common/withProviders.tsx +6 -2
- package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
- package/src/components/Popups/PopupV2.scss +4 -1
- package/src/components/Popups/PopupV2.tsx +122 -47
- package/src/document/DocumentAnnotator.ts +8 -4
- 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/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/image/ImageAnnotator.ts +3 -2
- package/src/media/MediaAnnotator.ts +2 -1
- package/src/popup/PopupContainer.tsx +8 -1
- package/src/popup/PopupLayer.tsx +41 -6
- package/src/popup/PopupManager.tsx +24 -4
- package/src/region/RegionCreation.tsx +11 -8
- package/src/region/RegionCreationContainer.tsx +2 -2
- package/src/region/RegionCreator.tsx +9 -17
- package/src/store/annotations/actions.ts +89 -0
- package/src/store/annotations/reducer.ts +48 -0
- package/src/store/eventing/create.ts +3 -3
- package/src/store/eventing/delete.ts +27 -0
- package/src/store/eventing/middleware.ts +40 -4
- 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/options/selectors.ts +2 -2
- package/src/store/options/types.ts +2 -2
- package/src/utils/rotate.ts +43 -0
|
@@ -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
|
},
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import noop from 'lodash/noop';
|
|
3
|
-
import
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom';
|
|
4
4
|
import { useIntl } from 'react-intl';
|
|
5
|
-
import {
|
|
5
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
|
+
|
|
7
|
+
import { BlueprintModernizationProvider, TooltipProvider } from '@box/blueprint-web';
|
|
6
8
|
import {
|
|
7
9
|
MentionContextProvider,
|
|
8
10
|
MessageEditorV2,
|
|
9
11
|
ThreadedAnnotationsV2,
|
|
10
12
|
serializeMentionMarkup,
|
|
13
|
+
serializeMessageToMarkdown,
|
|
11
14
|
} from '@box/threaded-annotations';
|
|
12
15
|
import type { DocumentNodeV2, TextMessageTypeV2 } from '@box/threaded-annotations';
|
|
13
16
|
import type { FetchedAvatarUrls, UserContactType } from '@box/user-selector';
|
|
@@ -15,16 +18,20 @@ import type { JSONContent } from '@tiptap/core';
|
|
|
15
18
|
import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
|
|
16
19
|
|
|
17
20
|
import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
|
|
21
|
+
import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext';
|
|
18
22
|
import {
|
|
19
23
|
createReplyAction,
|
|
20
24
|
deleteAnnotationAction,
|
|
25
|
+
deleteReplyAction,
|
|
21
26
|
setActiveAnnotationIdAction,
|
|
22
27
|
updateAnnotationAction,
|
|
28
|
+
updateReplyAction,
|
|
23
29
|
} from '../../store/annotations/actions';
|
|
24
30
|
import { getAnnotation } from '../../store/annotations/selectors';
|
|
25
|
-
import { getApiHost, getToken } from '../../store/options';
|
|
31
|
+
import { getApiHost, getFileId, getFileVersionId, getToken, isFeatureEnabled } from '../../store/options';
|
|
26
32
|
import { fetchCollaboratorsAction } from '../../store/users/actions';
|
|
27
33
|
|
|
34
|
+
import type { Token, TokenLiteral, TokenMap } from '../../@types';
|
|
28
35
|
import type { AppState, AppThunkDispatch } from '../../store/types';
|
|
29
36
|
|
|
30
37
|
import createPopper, { PopupReference } from './Popper';
|
|
@@ -35,6 +42,7 @@ import './PopupV2.scss';
|
|
|
35
42
|
export type Props = {
|
|
36
43
|
annotationId?: string;
|
|
37
44
|
onSubmit: (text: string) => void;
|
|
45
|
+
popupPortalEl?: HTMLElement | null;
|
|
38
46
|
reference: PopupReference;
|
|
39
47
|
};
|
|
40
48
|
|
|
@@ -59,12 +67,35 @@ const createDocumentNode = (content: JSONContent | null): DocumentNodeV2 => {
|
|
|
59
67
|
return { type: 'doc', content: [content] } as DocumentNodeV2;
|
|
60
68
|
};
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
const serializePopupMessage = (doc: DocumentNodeV2, isRichTextEnabled: boolean): string =>
|
|
71
|
+
isRichTextEnabled ? serializeMessageToMarkdown(doc) : serializeMentionMarkup(doc).text;
|
|
72
|
+
|
|
73
|
+
const literalToString = (literal: TokenLiteral): string | null => {
|
|
74
|
+
if (!literal) return null;
|
|
75
|
+
if (typeof literal === 'string') return literal;
|
|
76
|
+
return literal.read ?? literal.write ?? null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const resolveStringToken = async (token: Token, typedFileId: string): Promise<string | null> => {
|
|
80
|
+
const resolved = typeof token === 'function' ? await token(typedFileId) : token;
|
|
81
|
+
if (!resolved) return null;
|
|
82
|
+
if (typeof resolved === 'string') return resolved;
|
|
83
|
+
if (typedFileId in resolved) return literalToString((resolved as TokenMap)[typedFileId]);
|
|
84
|
+
return literalToString(resolved as TokenLiteral);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const fetchAvatarBlob = async (
|
|
88
|
+
apiHost: string,
|
|
89
|
+
token: Token,
|
|
90
|
+
fileId: string | null,
|
|
91
|
+
userId: string,
|
|
92
|
+
): Promise<string | null> => {
|
|
65
93
|
try {
|
|
94
|
+
if (!fileId) return null;
|
|
95
|
+
const stringToken = await resolveStringToken(token, `file_${fileId}`);
|
|
96
|
+
if (!stringToken) return null;
|
|
66
97
|
const response = await fetch(`${apiHost}/2.0/users/${userId}/avatar?pic_type=large`, {
|
|
67
|
-
headers: { Authorization: `Bearer ${
|
|
98
|
+
headers: { Authorization: `Bearer ${stringToken}` },
|
|
68
99
|
});
|
|
69
100
|
if (!response.ok) return null;
|
|
70
101
|
const blob = await response.blob();
|
|
@@ -74,15 +105,26 @@ const fetchAvatarBlob = async (apiHost: string, token: string, userId: string):
|
|
|
74
105
|
}
|
|
75
106
|
};
|
|
76
107
|
|
|
77
|
-
const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
108
|
+
const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
|
|
78
109
|
const intl = useIntl();
|
|
79
110
|
const dispatch = useDispatch<AppThunkDispatch>();
|
|
111
|
+
const { onCopyLink: consumerOnCopyLink } = React.useContext(AnnotationCallbacksContext);
|
|
80
112
|
const popupRef = React.useRef<HTMLDivElement>(null);
|
|
81
113
|
const popperRef = React.useRef<Instance>();
|
|
82
114
|
const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
|
|
83
115
|
|
|
84
116
|
const apiHost = useSelector(getApiHost);
|
|
117
|
+
const fileId = useSelector(getFileId);
|
|
118
|
+
const fileVersionId = useSelector(getFileVersionId);
|
|
119
|
+
const isRichTextEnabled = useSelector((state: AppState) => isFeatureEnabled(state, 'isRichTextEnabled'));
|
|
85
120
|
const token = useSelector(getToken);
|
|
121
|
+
const onCopyLink = React.useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
consumerOnCopyLink && annotationId && fileVersionId
|
|
124
|
+
? () => consumerOnCopyLink({ annotationId, fileVersionId })
|
|
125
|
+
: undefined,
|
|
126
|
+
[consumerOnCopyLink, annotationId, fileVersionId],
|
|
127
|
+
);
|
|
86
128
|
const annotation = useSelector((state: AppState) =>
|
|
87
129
|
annotationId ? getAnnotation(state, annotationId) : undefined,
|
|
88
130
|
);
|
|
@@ -98,7 +140,7 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
98
140
|
if (cached) return cached;
|
|
99
141
|
const capturedApiHost = apiHost;
|
|
100
142
|
const capturedToken = token;
|
|
101
|
-
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, userId);
|
|
143
|
+
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, fileId, userId);
|
|
102
144
|
if (!url) return null;
|
|
103
145
|
if (
|
|
104
146
|
credentialsRef.current.apiHost !== capturedApiHost ||
|
|
@@ -115,7 +157,7 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
115
157
|
avatarCacheRef.current.set(userId, url);
|
|
116
158
|
return url;
|
|
117
159
|
},
|
|
118
|
-
[apiHost, token],
|
|
160
|
+
[apiHost, fileId, token],
|
|
119
161
|
);
|
|
120
162
|
|
|
121
163
|
React.useEffect(() => {
|
|
@@ -150,7 +192,7 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
150
192
|
if (!annotation) return undefined;
|
|
151
193
|
|
|
152
194
|
const userIds = Array.from(
|
|
153
|
-
new Set(annotationToMessages(annotation).map(msg => String(msg.author.id))),
|
|
195
|
+
new Set(annotationToMessages(annotation, isRichTextEnabled).map(msg => String(msg.author.id))),
|
|
154
196
|
);
|
|
155
197
|
let cancelled = false;
|
|
156
198
|
|
|
@@ -168,18 +210,18 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
168
210
|
return () => {
|
|
169
211
|
cancelled = true;
|
|
170
212
|
};
|
|
171
|
-
}, [annotation, getOrFetchAvatarBlob]);
|
|
213
|
+
}, [annotation, getOrFetchAvatarBlob, isRichTextEnabled]);
|
|
172
214
|
|
|
173
215
|
const threadMessages: TextMessageTypeV2[] = React.useMemo(() => {
|
|
174
216
|
if (!annotation) return [];
|
|
175
|
-
return annotationToMessages(annotation).map(msg => ({
|
|
217
|
+
return annotationToMessages(annotation, isRichTextEnabled).map(msg => ({
|
|
176
218
|
...msg,
|
|
177
219
|
author: {
|
|
178
220
|
...msg.author,
|
|
179
221
|
avatarUrl: avatarBlobs[String(msg.author.id)],
|
|
180
222
|
},
|
|
181
223
|
}));
|
|
182
|
-
}, [annotation, avatarBlobs]);
|
|
224
|
+
}, [annotation, avatarBlobs, isRichTextEnabled]);
|
|
183
225
|
|
|
184
226
|
const isResolved = annotation?.status === 'resolved';
|
|
185
227
|
const resolvedBy = isResolved
|
|
@@ -222,14 +264,28 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
222
264
|
const handlePost = React.useCallback(
|
|
223
265
|
async (content: JSONContent | null): Promise<void> => {
|
|
224
266
|
const doc = createDocumentNode(content);
|
|
225
|
-
const
|
|
267
|
+
const text = serializePopupMessage(doc, isRichTextEnabled);
|
|
226
268
|
if (annotationId) {
|
|
227
269
|
await dispatch(createReplyAction({ annotationId, message: text }));
|
|
228
270
|
} else {
|
|
229
271
|
onSubmit(text);
|
|
230
272
|
}
|
|
231
273
|
},
|
|
232
|
-
[annotationId, dispatch, onSubmit],
|
|
274
|
+
[annotationId, dispatch, isRichTextEnabled, onSubmit],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const handleEdit = React.useCallback(
|
|
278
|
+
async (id: string, content: JSONContent | null): Promise<void> => {
|
|
279
|
+
if (!annotationId) return;
|
|
280
|
+
const doc = createDocumentNode(content);
|
|
281
|
+
const text = serializePopupMessage(doc, isRichTextEnabled);
|
|
282
|
+
if (id === annotationId) {
|
|
283
|
+
await dispatch(updateAnnotationAction({ annotationId, payload: { message: text } }));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
await dispatch(updateReplyAction({ annotationId, replyId: id, payload: { message: text } }));
|
|
287
|
+
},
|
|
288
|
+
[annotationId, dispatch, isRichTextEnabled],
|
|
233
289
|
);
|
|
234
290
|
|
|
235
291
|
const handleThreadDelete = React.useCallback(
|
|
@@ -241,6 +297,14 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
241
297
|
[annotationId, dispatch],
|
|
242
298
|
);
|
|
243
299
|
|
|
300
|
+
const handleDelete = React.useCallback(
|
|
301
|
+
async (id: string): Promise<void> => {
|
|
302
|
+
if (!annotationId || id === annotationId) return;
|
|
303
|
+
await dispatch(deleteReplyAction({ annotationId, replyId: id }));
|
|
304
|
+
},
|
|
305
|
+
[annotationId, dispatch],
|
|
306
|
+
);
|
|
307
|
+
|
|
244
308
|
const handleResolve = React.useCallback(
|
|
245
309
|
async (): Promise<void> => {
|
|
246
310
|
if (!annotationId) return;
|
|
@@ -260,7 +324,9 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
260
324
|
[annotationId, dispatch],
|
|
261
325
|
);
|
|
262
326
|
|
|
263
|
-
return
|
|
327
|
+
if (!popupPortalEl) return null;
|
|
328
|
+
|
|
329
|
+
return ReactDOM.createPortal(
|
|
264
330
|
<FocusTrap>
|
|
265
331
|
<div
|
|
266
332
|
ref={popupRef}
|
|
@@ -276,38 +342,47 @@ const PopupV2 = ({ annotationId, onSubmit, reference }: Props): JSX.Element => {
|
|
|
276
342
|
role="presentation"
|
|
277
343
|
>
|
|
278
344
|
<BlueprintModernizationProvider enableModernizedComponents>
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
345
|
+
<TooltipProvider>
|
|
346
|
+
<MentionContextProvider value={{
|
|
347
|
+
collaborationPopoverProps: {
|
|
348
|
+
onSubmit: () => Promise.resolve(),
|
|
349
|
+
},
|
|
350
|
+
// Mentions are restricted to file collaborators; the invite popover is intentionally never shown.
|
|
351
|
+
fetchCollaboratorState: async () => true,
|
|
352
|
+
}}>
|
|
353
|
+
{annotationId ? (
|
|
354
|
+
<ThreadedAnnotationsV2
|
|
355
|
+
isAnnotations
|
|
356
|
+
isResolved={isResolved}
|
|
357
|
+
isRichTextEnabled={isRichTextEnabled}
|
|
358
|
+
messages={threadMessages}
|
|
359
|
+
onAvatarClick={noop}
|
|
360
|
+
onCopyLink={onCopyLink}
|
|
361
|
+
onDelete={handleDelete}
|
|
362
|
+
onEdit={handleEdit}
|
|
363
|
+
onPost={handlePost}
|
|
364
|
+
onResolve={handleResolve}
|
|
365
|
+
onThreadDelete={handleThreadDelete}
|
|
366
|
+
onUnresolve={handleUnresolve}
|
|
367
|
+
resolvedAt={resolvedAt}
|
|
368
|
+
resolvedBy={resolvedBy}
|
|
369
|
+
userSelectorProps={userSelectorProps}
|
|
370
|
+
/>
|
|
371
|
+
) : (
|
|
372
|
+
<MessageEditorV2
|
|
373
|
+
isFirstAnnotation
|
|
374
|
+
isRichTextEnabled={isRichTextEnabled}
|
|
375
|
+
onPost={handlePost}
|
|
376
|
+
userSelectorProps={userSelectorProps}
|
|
377
|
+
/>
|
|
378
|
+
)}
|
|
379
|
+
</MentionContextProvider>
|
|
380
|
+
</TooltipProvider>
|
|
307
381
|
</BlueprintModernizationProvider>
|
|
308
382
|
<div data-threaded-annotations-portal />
|
|
309
383
|
</div>
|
|
310
|
-
</FocusTrap
|
|
384
|
+
</FocusTrap>,
|
|
385
|
+
popupPortalEl,
|
|
311
386
|
);
|
|
312
387
|
};
|
|
313
388
|
|
|
@@ -24,6 +24,9 @@ import type { ViewMode } from '../store/options/types';
|
|
|
24
24
|
// Pdf.js textLayer enhancement requires waiting for 300ms (they hardcode this magic number)
|
|
25
25
|
const TEXT_LAYER_ENHANCEMENT = 300;
|
|
26
26
|
|
|
27
|
+
// Vertical padding on each page element as provided by Preview SDK (see DocumentAnnotator.scss)
|
|
28
|
+
const PAGE_PADDING = 15;
|
|
29
|
+
|
|
27
30
|
export default class DocumentAnnotator extends BaseAnnotator {
|
|
28
31
|
annotatedEl?: HTMLElement;
|
|
29
32
|
|
|
@@ -99,8 +102,8 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
// Annotations mode
|
|
102
|
-
managers.add(new PopupManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
103
|
-
managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
105
|
+
managers.add(new PopupManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
106
|
+
managers.add(new DrawingManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
104
107
|
|
|
105
108
|
const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
|
|
106
109
|
|
|
@@ -115,7 +118,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
115
118
|
);
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
121
|
+
managers.add(new HighlightManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
119
122
|
managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
120
123
|
|
|
121
124
|
const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
|
|
@@ -200,7 +203,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
200
203
|
|
|
201
204
|
// Calculate original (unrotated) page dimensions for annotation layer sizing
|
|
202
205
|
const pageWidth = pageEl.clientWidth;
|
|
203
|
-
const pageHeight = pageEl.clientHeight;
|
|
206
|
+
const pageHeight = pageEl.clientHeight - PAGE_PADDING * 2;
|
|
204
207
|
const isOrthogonal = rotation % 180 !== 0;
|
|
205
208
|
const origWidth = isOrthogonal ? pageHeight : pageWidth;
|
|
206
209
|
const origHeight = isOrthogonal ? pageWidth : pageHeight;
|
|
@@ -221,6 +224,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
manager.render({
|
|
227
|
+
callbacks: { onCopyLink: this.onCopyLink },
|
|
224
228
|
intl: this.intl,
|
|
225
229
|
store: this.store,
|
|
226
230
|
});
|
package/src/document/docUtil.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
type Selection = {
|
|
2
|
+
containerEl: HTMLElement;
|
|
2
3
|
containerRect: DOMRect;
|
|
3
4
|
hasError?: boolean;
|
|
4
5
|
location: number;
|
|
@@ -61,5 +62,5 @@ export function getSelection(): Selection | null {
|
|
|
61
62
|
location = getPageNumber(selection.focusNode as Element) ?? endPage;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
return { containerRect: containerEl.getBoundingClientRect(), hasError, location, range };
|
|
65
|
+
return { containerEl: containerEl as HTMLElement, containerRect: containerEl.getBoundingClientRect(), hasError, location, range };
|
|
65
66
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
2
3
|
import classNames from 'classnames';
|
|
3
4
|
import DecoratedDrawingPath from './DecoratedDrawingPath';
|
|
4
5
|
import DrawingList from './DrawingList';
|
|
@@ -22,8 +23,10 @@ export type Props = {
|
|
|
22
23
|
drawnPathGroups: Array<PathGroup>;
|
|
23
24
|
isCreating: boolean;
|
|
24
25
|
location: number;
|
|
26
|
+
popupPortalEl?: HTMLElement | null;
|
|
25
27
|
redoDrawingPathGroup: () => void;
|
|
26
28
|
resetDrawing: () => void;
|
|
29
|
+
rotation: number;
|
|
27
30
|
setActiveAnnotationId: (annotationId: string | null) => void;
|
|
28
31
|
setReferenceId: (uuid: string) => void;
|
|
29
32
|
setStaged: (staged: CreatorItemDrawing | null) => void;
|
|
@@ -45,8 +48,10 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
45
48
|
drawnPathGroups,
|
|
46
49
|
isCreating,
|
|
47
50
|
location,
|
|
51
|
+
popupPortalEl,
|
|
48
52
|
redoDrawingPathGroup,
|
|
49
53
|
resetDrawing,
|
|
54
|
+
rotation,
|
|
50
55
|
setActiveAnnotationId,
|
|
51
56
|
setReferenceId,
|
|
52
57
|
setStaged,
|
|
@@ -170,23 +175,27 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
170
175
|
color={color}
|
|
171
176
|
onStart={handleStart}
|
|
172
177
|
onStop={handleStop}
|
|
178
|
+
rotation={rotation}
|
|
173
179
|
/>
|
|
174
180
|
)}
|
|
175
181
|
|
|
176
|
-
{isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
{isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (() => {
|
|
183
|
+
const toolbar = (
|
|
184
|
+
<PopupDrawingToolbar
|
|
185
|
+
ref={popupDrawingToolbarRef}
|
|
186
|
+
canComment={hasDrawnPathGroups}
|
|
187
|
+
canRedo={hasStashedPathGroups}
|
|
188
|
+
canUndo={hasDrawnPathGroups}
|
|
189
|
+
className={classNames('ba-DrawingAnnotations-toolbar', { 'ba-is-drawing': isDrawing })}
|
|
190
|
+
onDelete={handleDelete}
|
|
191
|
+
onRedo={handleRedo}
|
|
192
|
+
onReply={handleReply}
|
|
193
|
+
onUndo={handleUndo}
|
|
194
|
+
reference={drawingSVGGroup}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
return popupPortalEl ? ReactDOM.createPortal(toolbar, popupPortalEl) : toolbar;
|
|
198
|
+
})()}
|
|
190
199
|
</>
|
|
191
200
|
);
|
|
192
201
|
};
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getAnnotationsForLocation,
|
|
19
19
|
getColor,
|
|
20
20
|
getCreatorStatus,
|
|
21
|
+
getRotation,
|
|
21
22
|
Mode,
|
|
22
23
|
setActiveAnnotationIdAction,
|
|
23
24
|
setReferenceIdAction,
|
|
@@ -34,6 +35,7 @@ export type Props = {
|
|
|
34
35
|
canShowPopupToolbar: boolean;
|
|
35
36
|
drawnPathGroups: Array<PathGroup>;
|
|
36
37
|
isCreating: boolean;
|
|
38
|
+
rotation: number;
|
|
37
39
|
stashedPathGroups: Array<PathGroup>;
|
|
38
40
|
};
|
|
39
41
|
|
|
@@ -47,6 +49,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
|
|
|
47
49
|
canShowPopupToolbar: creatorStatus === CreatorStatus.started,
|
|
48
50
|
drawnPathGroups: getDrawingDrawnPathGroupsForLocation(state, location),
|
|
49
51
|
isCreating: getAnnotationMode(state) === Mode.DRAWING && creatorStatus !== CreatorStatus.pending,
|
|
52
|
+
rotation: getRotation(state),
|
|
50
53
|
stashedPathGroups: getStashedDrawnPathGroupsForLocation(state, location),
|
|
51
54
|
};
|
|
52
55
|
};
|
|
@@ -7,6 +7,7 @@ import DrawingSVG, { DrawingSVGRef } from './DrawingSVG';
|
|
|
7
7
|
import PointerCapture, { PointerCaptureRef, Status as DrawingStatus } from '../components/PointerCapture';
|
|
8
8
|
import { getDrawingCursor } from './DrawingCursor';
|
|
9
9
|
import { getPathCommands } from './drawingUtil';
|
|
10
|
+
import { getElementLocalPosition } from '../utils/rotate';
|
|
10
11
|
import { PathGroup, Position } from '../@types';
|
|
11
12
|
import './DrawingCreator.scss';
|
|
12
13
|
|
|
@@ -15,6 +16,7 @@ export type Props = {
|
|
|
15
16
|
color?: string;
|
|
16
17
|
onStart: () => void;
|
|
17
18
|
onStop: (pathGroup: PathGroup) => void;
|
|
19
|
+
rotation?: number;
|
|
18
20
|
size?: number;
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -26,6 +28,7 @@ export default function DrawingCreator({
|
|
|
26
28
|
color = defaultStrokeColor,
|
|
27
29
|
onStart,
|
|
28
30
|
onStop,
|
|
31
|
+
rotation = 0,
|
|
29
32
|
size = defaultStrokeSize,
|
|
30
33
|
}: Props): JSX.Element {
|
|
31
34
|
const [drawingStatus, setDrawingStatus] = React.useState<DrawingStatus>(DrawingStatus.init);
|
|
@@ -45,7 +48,9 @@ export default function DrawingCreator({
|
|
|
45
48
|
return [];
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
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
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as ReactDOM from 'react-dom/client';
|
|
3
|
-
import BaseManager, { Props } from '../common/BaseManager';
|
|
3
|
+
import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
|
|
4
4
|
import DrawingAnnotationsContainer from './DrawingAnnotationsContainer';
|
|
5
5
|
|
|
6
|
+
export type Options = BaseOptions & {
|
|
7
|
+
popupPortalEl?: HTMLElement | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
6
10
|
export default class DrawingListManager extends BaseManager {
|
|
11
|
+
popupPortalEl?: HTMLElement | null;
|
|
12
|
+
|
|
13
|
+
constructor({ popupPortalEl, ...options }: Options) {
|
|
14
|
+
super(options);
|
|
15
|
+
this.popupPortalEl = popupPortalEl;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
decorate(): void {
|
|
8
19
|
this.reactEl.classList.add('ba-Layer--drawing');
|
|
9
20
|
this.reactEl.dataset.testid = 'ba-Layer--drawing';
|
|
@@ -14,6 +25,6 @@ export default class DrawingListManager extends BaseManager {
|
|
|
14
25
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} {...props} />);
|
|
28
|
+
this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
|
|
18
29
|
}
|
|
19
30
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
2
3
|
import HighlightCanvas from './HighlightCanvas';
|
|
3
4
|
import HighlightList from './HighlightList';
|
|
4
5
|
import HighlightSvg from './HighlightSvg';
|
|
@@ -20,6 +21,8 @@ type Props = {
|
|
|
20
21
|
isPromoting: boolean;
|
|
21
22
|
isSelecting: boolean;
|
|
22
23
|
location: number;
|
|
24
|
+
popupPortalEl?: HTMLElement | null;
|
|
25
|
+
rotation: number;
|
|
23
26
|
selection: SelectionItem | null;
|
|
24
27
|
setActiveAnnotationId: (annotationId: string | null) => void;
|
|
25
28
|
setIsPromoting: (isPromoting: boolean) => void;
|
|
@@ -38,6 +41,8 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
|
|
|
38
41
|
isPending = false,
|
|
39
42
|
isPromoting = false,
|
|
40
43
|
isSelecting = false,
|
|
44
|
+
popupPortalEl,
|
|
45
|
+
rotation,
|
|
41
46
|
selection,
|
|
42
47
|
setActiveAnnotationId,
|
|
43
48
|
setIsPromoting,
|
|
@@ -121,22 +126,30 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
|
|
|
121
126
|
)}
|
|
122
127
|
|
|
123
128
|
{/* Layer 3a: Annotations promoter to promote selection to staged */}
|
|
124
|
-
{!isCreating && selection && !selection.hasError && (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
{!isCreating && selection && !selection.hasError && (() => {
|
|
130
|
+
const popupShape = selection.popupRect || getBoundingRect(selection.rects);
|
|
131
|
+
const popup = (
|
|
132
|
+
<div className="ba-HighlightAnnotations-popup">
|
|
133
|
+
<PopupHighlight
|
|
134
|
+
onCancel={handleCancel}
|
|
135
|
+
onSubmit={handlePromote}
|
|
136
|
+
shape={popupShape}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
return rotation && popupPortalEl ? ReactDOM.createPortal(popup, popupPortalEl) : popup;
|
|
141
|
+
})()}
|
|
133
142
|
|
|
134
143
|
{/* Layer 3b: Highlight error popup */}
|
|
135
|
-
{selection && selection.hasError && (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
{selection && selection.hasError && (() => {
|
|
145
|
+
const popupShape = selection.popupRect || getBoundingRect(selection.rects);
|
|
146
|
+
const popup = (
|
|
147
|
+
<div className="ba-HighlightAnnotations-popup">
|
|
148
|
+
<PopupHighlightError onCancel={handleCancel} shape={popupShape} />
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
return rotation && popupPortalEl ? ReactDOM.createPortal(popup, popupPortalEl) : popup;
|
|
152
|
+
})()}
|
|
140
153
|
</>
|
|
141
154
|
);
|
|
142
155
|
};
|