box-annotations 5.2.1-beta.8 → 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 +12 -0
- package/src/common/BaseAnnotator.ts +32 -2
- package/src/common/BaseManager.ts +2 -0
- package/src/common/DeselectListener.tsx +1 -1
- package/src/common/withProviders.tsx +6 -2
- package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
- package/src/components/Popups/PopupV2.tsx +111 -42
- package/src/document/DocumentAnnotator.ts +3 -2
- 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 +2 -1
- package/src/media/MediaAnnotator.ts +1 -0
- package/src/popup/PopupLayer.tsx +37 -6
- package/src/popup/PopupManager.tsx +4 -2
- 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
|
@@ -4,12 +4,13 @@ import ReactDOM from 'react-dom';
|
|
|
4
4
|
import { useIntl } from 'react-intl';
|
|
5
5
|
import { useDispatch, useSelector } from 'react-redux';
|
|
6
6
|
|
|
7
|
-
import { BlueprintModernizationProvider } from '@box/blueprint-web';
|
|
7
|
+
import { BlueprintModernizationProvider, TooltipProvider } from '@box/blueprint-web';
|
|
8
8
|
import {
|
|
9
9
|
MentionContextProvider,
|
|
10
10
|
MessageEditorV2,
|
|
11
11
|
ThreadedAnnotationsV2,
|
|
12
12
|
serializeMentionMarkup,
|
|
13
|
+
serializeMessageToMarkdown,
|
|
13
14
|
} from '@box/threaded-annotations';
|
|
14
15
|
import type { DocumentNodeV2, TextMessageTypeV2 } from '@box/threaded-annotations';
|
|
15
16
|
import type { FetchedAvatarUrls, UserContactType } from '@box/user-selector';
|
|
@@ -17,16 +18,20 @@ import type { JSONContent } from '@tiptap/core';
|
|
|
17
18
|
import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
|
|
18
19
|
|
|
19
20
|
import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
|
|
21
|
+
import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext';
|
|
20
22
|
import {
|
|
21
23
|
createReplyAction,
|
|
22
24
|
deleteAnnotationAction,
|
|
25
|
+
deleteReplyAction,
|
|
23
26
|
setActiveAnnotationIdAction,
|
|
24
27
|
updateAnnotationAction,
|
|
28
|
+
updateReplyAction,
|
|
25
29
|
} from '../../store/annotations/actions';
|
|
26
30
|
import { getAnnotation } from '../../store/annotations/selectors';
|
|
27
|
-
import { getApiHost, getToken } from '../../store/options';
|
|
31
|
+
import { getApiHost, getFileId, getFileVersionId, getToken, isFeatureEnabled } from '../../store/options';
|
|
28
32
|
import { fetchCollaboratorsAction } from '../../store/users/actions';
|
|
29
33
|
|
|
34
|
+
import type { Token, TokenLiteral, TokenMap } from '../../@types';
|
|
30
35
|
import type { AppState, AppThunkDispatch } from '../../store/types';
|
|
31
36
|
|
|
32
37
|
import createPopper, { PopupReference } from './Popper';
|
|
@@ -62,12 +67,35 @@ const createDocumentNode = (content: JSONContent | null): DocumentNodeV2 => {
|
|
|
62
67
|
return { type: 'doc', content: [content] } as DocumentNodeV2;
|
|
63
68
|
};
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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> => {
|
|
68
93
|
try {
|
|
94
|
+
if (!fileId) return null;
|
|
95
|
+
const stringToken = await resolveStringToken(token, `file_${fileId}`);
|
|
96
|
+
if (!stringToken) return null;
|
|
69
97
|
const response = await fetch(`${apiHost}/2.0/users/${userId}/avatar?pic_type=large`, {
|
|
70
|
-
headers: { Authorization: `Bearer ${
|
|
98
|
+
headers: { Authorization: `Bearer ${stringToken}` },
|
|
71
99
|
});
|
|
72
100
|
if (!response.ok) return null;
|
|
73
101
|
const blob = await response.blob();
|
|
@@ -80,12 +108,23 @@ const fetchAvatarBlob = async (apiHost: string, token: string, userId: string):
|
|
|
80
108
|
const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
|
|
81
109
|
const intl = useIntl();
|
|
82
110
|
const dispatch = useDispatch<AppThunkDispatch>();
|
|
111
|
+
const { onCopyLink: consumerOnCopyLink } = React.useContext(AnnotationCallbacksContext);
|
|
83
112
|
const popupRef = React.useRef<HTMLDivElement>(null);
|
|
84
113
|
const popperRef = React.useRef<Instance>();
|
|
85
114
|
const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
|
|
86
115
|
|
|
87
116
|
const apiHost = useSelector(getApiHost);
|
|
117
|
+
const fileId = useSelector(getFileId);
|
|
118
|
+
const fileVersionId = useSelector(getFileVersionId);
|
|
119
|
+
const isRichTextEnabled = useSelector((state: AppState) => isFeatureEnabled(state, 'isRichTextEnabled'));
|
|
88
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
|
+
);
|
|
89
128
|
const annotation = useSelector((state: AppState) =>
|
|
90
129
|
annotationId ? getAnnotation(state, annotationId) : undefined,
|
|
91
130
|
);
|
|
@@ -101,7 +140,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
101
140
|
if (cached) return cached;
|
|
102
141
|
const capturedApiHost = apiHost;
|
|
103
142
|
const capturedToken = token;
|
|
104
|
-
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, userId);
|
|
143
|
+
const url = await fetchAvatarBlob(capturedApiHost, capturedToken, fileId, userId);
|
|
105
144
|
if (!url) return null;
|
|
106
145
|
if (
|
|
107
146
|
credentialsRef.current.apiHost !== capturedApiHost ||
|
|
@@ -118,7 +157,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
118
157
|
avatarCacheRef.current.set(userId, url);
|
|
119
158
|
return url;
|
|
120
159
|
},
|
|
121
|
-
[apiHost, token],
|
|
160
|
+
[apiHost, fileId, token],
|
|
122
161
|
);
|
|
123
162
|
|
|
124
163
|
React.useEffect(() => {
|
|
@@ -153,7 +192,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
153
192
|
if (!annotation) return undefined;
|
|
154
193
|
|
|
155
194
|
const userIds = Array.from(
|
|
156
|
-
new Set(annotationToMessages(annotation).map(msg => String(msg.author.id))),
|
|
195
|
+
new Set(annotationToMessages(annotation, isRichTextEnabled).map(msg => String(msg.author.id))),
|
|
157
196
|
);
|
|
158
197
|
let cancelled = false;
|
|
159
198
|
|
|
@@ -171,18 +210,18 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
171
210
|
return () => {
|
|
172
211
|
cancelled = true;
|
|
173
212
|
};
|
|
174
|
-
}, [annotation, getOrFetchAvatarBlob]);
|
|
213
|
+
}, [annotation, getOrFetchAvatarBlob, isRichTextEnabled]);
|
|
175
214
|
|
|
176
215
|
const threadMessages: TextMessageTypeV2[] = React.useMemo(() => {
|
|
177
216
|
if (!annotation) return [];
|
|
178
|
-
return annotationToMessages(annotation).map(msg => ({
|
|
217
|
+
return annotationToMessages(annotation, isRichTextEnabled).map(msg => ({
|
|
179
218
|
...msg,
|
|
180
219
|
author: {
|
|
181
220
|
...msg.author,
|
|
182
221
|
avatarUrl: avatarBlobs[String(msg.author.id)],
|
|
183
222
|
},
|
|
184
223
|
}));
|
|
185
|
-
}, [annotation, avatarBlobs]);
|
|
224
|
+
}, [annotation, avatarBlobs, isRichTextEnabled]);
|
|
186
225
|
|
|
187
226
|
const isResolved = annotation?.status === 'resolved';
|
|
188
227
|
const resolvedBy = isResolved
|
|
@@ -225,14 +264,28 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
225
264
|
const handlePost = React.useCallback(
|
|
226
265
|
async (content: JSONContent | null): Promise<void> => {
|
|
227
266
|
const doc = createDocumentNode(content);
|
|
228
|
-
const
|
|
267
|
+
const text = serializePopupMessage(doc, isRichTextEnabled);
|
|
229
268
|
if (annotationId) {
|
|
230
269
|
await dispatch(createReplyAction({ annotationId, message: text }));
|
|
231
270
|
} else {
|
|
232
271
|
onSubmit(text);
|
|
233
272
|
}
|
|
234
273
|
},
|
|
235
|
-
[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],
|
|
236
289
|
);
|
|
237
290
|
|
|
238
291
|
const handleThreadDelete = React.useCallback(
|
|
@@ -244,6 +297,14 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
244
297
|
[annotationId, dispatch],
|
|
245
298
|
);
|
|
246
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
|
+
|
|
247
308
|
const handleResolve = React.useCallback(
|
|
248
309
|
async (): Promise<void> => {
|
|
249
310
|
if (!annotationId) return;
|
|
@@ -281,34 +342,42 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
|
|
|
281
342
|
role="presentation"
|
|
282
343
|
>
|
|
283
344
|
<BlueprintModernizationProvider enableModernizedComponents>
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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>
|
|
312
381
|
</BlueprintModernizationProvider>
|
|
313
382
|
<div data-threaded-annotations-portal />
|
|
314
383
|
</div>
|
|
@@ -103,7 +103,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
103
103
|
|
|
104
104
|
// Annotations mode
|
|
105
105
|
managers.add(new PopupManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
106
|
-
managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
106
|
+
managers.add(new DrawingManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
107
107
|
|
|
108
108
|
const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
|
|
109
109
|
|
|
@@ -118,7 +118,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
121
|
+
managers.add(new HighlightManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
|
|
122
122
|
managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
|
|
123
123
|
|
|
124
124
|
const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
|
|
@@ -224,6 +224,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
manager.render({
|
|
227
|
+
callbacks: { onCopyLink: this.onCopyLink },
|
|
227
228
|
intl: this.intl,
|
|
228
229
|
store: this.store,
|
|
229
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
|
};
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getCreatorStatus,
|
|
14
14
|
getIsPromoting,
|
|
15
15
|
getIsSelecting,
|
|
16
|
+
getRotation,
|
|
16
17
|
getSelectionForLocation,
|
|
17
18
|
isCreatorStagedHighlight,
|
|
18
19
|
Mode,
|
|
@@ -33,6 +34,7 @@ export type Props = {
|
|
|
33
34
|
isPending: boolean;
|
|
34
35
|
isPromoting: boolean;
|
|
35
36
|
isSelecting: boolean;
|
|
37
|
+
rotation: number;
|
|
36
38
|
selection: SelectionItem | null;
|
|
37
39
|
staged: CreatorItemHighlight | null;
|
|
38
40
|
};
|
|
@@ -47,6 +49,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
|
|
|
47
49
|
isPending: getCreatorStatus(state) === CreatorStatus.pending,
|
|
48
50
|
isPromoting: getIsPromoting(state),
|
|
49
51
|
isSelecting: getIsSelecting(state),
|
|
52
|
+
rotation: getRotation(state),
|
|
50
53
|
selection: getSelectionForLocation(state, location),
|
|
51
54
|
staged: isCreatorStagedHighlight(staged) ? staged : null,
|
|
52
55
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AppStore,
|
|
3
3
|
getIsSelecting,
|
|
4
|
+
getRotation,
|
|
4
5
|
SelectionArg as Selection,
|
|
5
6
|
setIsSelectingAction,
|
|
6
7
|
setSelectionAction,
|
|
@@ -41,18 +42,18 @@ export default class HighlightCreatorManager implements Manager {
|
|
|
41
42
|
this.store.dispatch(setSelectionAction(null));
|
|
42
43
|
|
|
43
44
|
this.referenceEl.addEventListener('mousedown', this.handleMouseDown);
|
|
44
|
-
|
|
45
|
+
document.addEventListener('mouseup', this.handleMouseUp);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
style(
|
|
48
|
-
return
|
|
48
|
+
style(): CSSStyleDeclaration {
|
|
49
|
+
return this.referenceEl.style;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
destroy(): void {
|
|
52
53
|
clearTimeout(this.selectionChangeTimer);
|
|
53
54
|
|
|
54
55
|
this.referenceEl.removeEventListener('mousedown', this.handleMouseDown);
|
|
55
|
-
|
|
56
|
+
document.removeEventListener('mouseup', this.handleMouseUp);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
handleMouseDown = ({ buttons }: MouseEvent): void => {
|
|
@@ -72,7 +73,9 @@ export default class HighlightCreatorManager implements Manager {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
this.selectionChangeTimer = window.setTimeout(() => {
|
|
75
|
-
this.
|
|
76
|
+
const selection = this.getSelection();
|
|
77
|
+
const rotation = getRotation(this.store.getState());
|
|
78
|
+
this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
|
|
76
79
|
this.store.dispatch(setIsSelectingAction(false));
|
|
77
80
|
}, this.selectionChangeDelay);
|
|
78
81
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import debounce from 'lodash/debounce';
|
|
2
|
-
import { AppStore, getIsSelecting, SelectionArg as Selection, setSelectionAction } from '../store';
|
|
2
|
+
import { AppStore, getIsSelecting, getRotation, SelectionArg as Selection, setSelectionAction } from '../store';
|
|
3
3
|
|
|
4
4
|
export type Options = {
|
|
5
5
|
getSelection: () => Selection | null;
|
|
@@ -30,7 +30,9 @@ export default class HighlightListener {
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
this.
|
|
33
|
+
const selection = this.getSelection();
|
|
34
|
+
const rotation = getRotation(this.store.getState());
|
|
35
|
+
this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
debounceHandleSelectionChange = debounce(this.handleSelectionChange, SELECTION_CHANGE_DEBOUNCE);
|