box-annotations 5.2.1-beta.8 → 5.3.1-beta.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.
Files changed (121) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/dist/annotations.js.LICENSE.txt +1 -10
  4. package/i18n/bn-IN.js +6 -0
  5. package/i18n/bn-IN.properties +12 -0
  6. package/i18n/da-DK.js +6 -0
  7. package/i18n/da-DK.properties +12 -0
  8. package/i18n/de-DE.js +6 -0
  9. package/i18n/de-DE.properties +12 -0
  10. package/i18n/en-AU.js +6 -0
  11. package/i18n/en-AU.properties +12 -0
  12. package/i18n/en-CA.js +6 -0
  13. package/i18n/en-CA.properties +12 -0
  14. package/i18n/en-GB.js +6 -0
  15. package/i18n/en-GB.properties +12 -0
  16. package/i18n/es-419.js +6 -0
  17. package/i18n/es-419.properties +12 -0
  18. package/i18n/es-ES.js +6 -0
  19. package/i18n/es-ES.properties +12 -0
  20. package/i18n/fi-FI.js +6 -0
  21. package/i18n/fi-FI.properties +12 -0
  22. package/i18n/fr-CA.js +6 -0
  23. package/i18n/fr-CA.properties +12 -0
  24. package/i18n/fr-FR.js +6 -0
  25. package/i18n/fr-FR.properties +12 -0
  26. package/i18n/hi-IN.js +6 -0
  27. package/i18n/hi-IN.properties +12 -0
  28. package/i18n/it-IT.js +6 -0
  29. package/i18n/it-IT.properties +12 -0
  30. package/i18n/ja-JP.js +7 -1
  31. package/i18n/ja-JP.properties +13 -1
  32. package/i18n/ko-KR.js +6 -0
  33. package/i18n/ko-KR.properties +12 -0
  34. package/i18n/nb-NO.js +6 -0
  35. package/i18n/nb-NO.properties +12 -0
  36. package/i18n/nl-NL.js +6 -0
  37. package/i18n/nl-NL.properties +12 -0
  38. package/i18n/pl-PL.js +6 -0
  39. package/i18n/pl-PL.properties +12 -0
  40. package/i18n/pt-BR.js +6 -0
  41. package/i18n/pt-BR.properties +12 -0
  42. package/i18n/ru-RU.js +6 -0
  43. package/i18n/ru-RU.properties +12 -0
  44. package/i18n/sv-SE.js +6 -0
  45. package/i18n/sv-SE.properties +12 -0
  46. package/i18n/tr-TR.js +6 -0
  47. package/i18n/tr-TR.properties +12 -0
  48. package/i18n/zh-CN.js +6 -0
  49. package/i18n/zh-CN.properties +12 -0
  50. package/i18n/zh-TW.js +6 -0
  51. package/i18n/zh-TW.properties +12 -0
  52. package/index.js +1 -1
  53. package/package.json +54 -42
  54. package/src/@types/api.ts +3 -2
  55. package/src/@types/events.ts +27 -10
  56. package/src/@types/global.ts +0 -9
  57. package/src/@types/model.ts +9 -0
  58. package/src/BoxAnnotations.ts +6 -2
  59. package/src/adapters/threadedAnnotationsAdapters.ts +49 -28
  60. package/src/api/APIFactory.ts +6 -1
  61. package/src/api/types.ts +23 -1
  62. package/src/boundingBoxHighlight/BoundingBoxHighlightContainer.tsx +4 -1
  63. package/src/common/AnnotationCallbacksContext.ts +7 -0
  64. package/src/common/BaseAnnotator.scss +12 -0
  65. package/src/common/BaseAnnotator.ts +32 -2
  66. package/src/common/BaseManager.ts +2 -0
  67. package/src/common/DeselectListener.tsx +1 -1
  68. package/src/common/withProviders.tsx +6 -2
  69. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightNav.scss +3 -3
  70. package/src/components/BoundingBoxHighlight/BoundingBoxHighlightRect.scss +11 -6
  71. package/src/components/Popups/PopupBase.scss +2 -2
  72. package/src/components/Popups/PopupDrawingToolbar.scss +4 -4
  73. package/src/components/Popups/PopupDrawingToolbar.tsx +0 -7
  74. package/src/components/Popups/PopupList.scss +1 -1
  75. package/src/components/Popups/PopupV2.scss +2 -2
  76. package/src/components/Popups/PopupV2.tsx +128 -72
  77. package/src/components/ReplyForm/ReplyForm.tsx +1 -8
  78. package/src/constants.ts +2 -3
  79. package/src/document/DocumentAnnotator.ts +29 -5
  80. package/src/document/docUtil.ts +8 -1
  81. package/src/drawing/DrawingAnnotations.tsx +30 -21
  82. package/src/drawing/DrawingAnnotationsContainer.tsx +3 -0
  83. package/src/drawing/DrawingCreator.tsx +9 -16
  84. package/src/drawing/DrawingCursor.ts +2 -1
  85. package/src/drawing/DrawingManager.tsx +21 -2
  86. package/src/highlight/HighlightAnnotations.tsx +28 -14
  87. package/src/highlight/HighlightContainer.tsx +3 -0
  88. package/src/highlight/HighlightCreator.scss +9 -2
  89. package/src/highlight/HighlightCreatorManager.ts +8 -5
  90. package/src/highlight/HighlightListener.ts +4 -2
  91. package/src/highlight/HighlightManager.tsx +13 -2
  92. package/src/image/ImageAnnotator.ts +2 -1
  93. package/src/media/MediaAnnotator.ts +39 -16
  94. package/src/popup/PopupLayer.tsx +38 -11
  95. package/src/popup/PopupManager.tsx +4 -2
  96. package/src/region/RegionAnnotations.tsx +2 -3
  97. package/src/region/RegionCreation.tsx +15 -9
  98. package/src/region/RegionCreationContainer.tsx +2 -2
  99. package/src/region/RegionCreationManager.tsx +8 -1
  100. package/src/region/RegionCreator.scss +9 -3
  101. package/src/region/RegionCreator.tsx +9 -17
  102. package/src/region/RegionManager.tsx +8 -1
  103. package/src/region/RegionRect.scss +17 -9
  104. package/src/store/annotations/actions.ts +104 -33
  105. package/src/store/annotations/reducer.ts +49 -1
  106. package/src/store/creator/selectors.ts +6 -6
  107. package/src/store/eventing/create.ts +6 -4
  108. package/src/store/eventing/delete.ts +26 -0
  109. package/src/store/eventing/middleware.ts +53 -15
  110. package/src/store/eventing/replyCreate.ts +30 -0
  111. package/src/store/eventing/replyDelete.ts +31 -0
  112. package/src/store/eventing/replyUpdate.ts +35 -0
  113. package/src/store/eventing/types.ts +4 -4
  114. package/src/store/eventing/update.ts +28 -0
  115. package/src/store/highlight/actions.ts +50 -19
  116. package/src/store/highlight/types.ts +1 -0
  117. package/src/store/options/selectors.ts +2 -2
  118. package/src/store/options/types.ts +2 -2
  119. package/src/test-utils/videoAnnotations.tsx +11 -11
  120. package/src/utils/rotate.ts +43 -0
  121. package/src/utils/useVideoTiming.ts +9 -13
@@ -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
- // 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> => {
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 ${token}` },
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,12 +140,9 @@ 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
- if (
107
- credentialsRef.current.apiHost !== capturedApiHost ||
108
- credentialsRef.current.token !== capturedToken
109
- ) {
145
+ if (credentialsRef.current.apiHost !== capturedApiHost || credentialsRef.current.token !== capturedToken) {
110
146
  URL.revokeObjectURL(url);
111
147
  return null;
112
148
  }
@@ -118,7 +154,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
118
154
  avatarCacheRef.current.set(userId, url);
119
155
  return url;
120
156
  },
121
- [apiHost, token],
157
+ [apiHost, fileId, token],
122
158
  );
123
159
 
124
160
  React.useEffect(() => {
@@ -153,7 +189,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
153
189
  if (!annotation) return undefined;
154
190
 
155
191
  const userIds = Array.from(
156
- new Set(annotationToMessages(annotation).map(msg => String(msg.author.id))),
192
+ new Set(annotationToMessages(annotation, isRichTextEnabled).map(msg => String(msg.author.id))),
157
193
  );
158
194
  let cancelled = false;
159
195
 
@@ -171,26 +207,24 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
171
207
  return () => {
172
208
  cancelled = true;
173
209
  };
174
- }, [annotation, getOrFetchAvatarBlob]);
210
+ }, [annotation, getOrFetchAvatarBlob, isRichTextEnabled]);
175
211
 
176
212
  const threadMessages: TextMessageTypeV2[] = React.useMemo(() => {
177
213
  if (!annotation) return [];
178
- return annotationToMessages(annotation).map(msg => ({
214
+ return annotationToMessages(annotation, isRichTextEnabled).map(msg => ({
179
215
  ...msg,
180
216
  author: {
181
217
  ...msg.author,
182
218
  avatarUrl: avatarBlobs[String(msg.author.id)],
183
219
  },
184
220
  }));
185
- }, [annotation, avatarBlobs]);
221
+ }, [annotation, avatarBlobs, isRichTextEnabled]);
186
222
 
187
223
  const isResolved = annotation?.status === 'resolved';
188
224
  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
225
+ ? (annotation?.resolution?.resolved_by?.name ?? annotation?.modified_by?.name)
193
226
  : undefined;
227
+ const resolvedAtSource = isResolved ? (annotation?.resolution?.resolved_at ?? annotation?.modified_at) : undefined;
194
228
  const resolvedAt = resolvedAtSource ? new Date(resolvedAtSource).getTime() : undefined;
195
229
 
196
230
  const userSelectorProps = React.useMemo(
@@ -225,44 +259,57 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
225
259
  const handlePost = React.useCallback(
226
260
  async (content: JSONContent | null): Promise<void> => {
227
261
  const doc = createDocumentNode(content);
228
- const { text } = serializeMentionMarkup(doc);
262
+ const text = serializePopupMessage(doc, isRichTextEnabled);
229
263
  if (annotationId) {
230
264
  await dispatch(createReplyAction({ annotationId, message: text }));
231
265
  } else {
232
266
  onSubmit(text);
233
267
  }
234
268
  },
235
- [annotationId, dispatch, onSubmit],
269
+ [annotationId, dispatch, isRichTextEnabled, onSubmit],
236
270
  );
237
271
 
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> => {
272
+ const handleEdit = React.useCallback(
273
+ async (id: string, content: JSONContent | null): Promise<void> => {
249
274
  if (!annotationId) return;
250
- const result = await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'resolved' } }));
251
- if (updateAnnotationAction.fulfilled.match(result)) {
252
- dispatch(setActiveAnnotationIdAction(null));
275
+ const doc = createDocumentNode(content);
276
+ const text = serializePopupMessage(doc, isRichTextEnabled);
277
+ if (id === annotationId) {
278
+ await dispatch(updateAnnotationAction({ annotationId, payload: { message: text } }));
279
+ return;
253
280
  }
281
+ await dispatch(updateReplyAction({ annotationId, replyId: id, payload: { message: text } }));
254
282
  },
255
- [annotationId, dispatch],
283
+ [annotationId, dispatch, isRichTextEnabled],
256
284
  );
257
285
 
258
- const handleUnresolve = React.useCallback(
259
- async (): Promise<void> => {
260
- if (!annotationId) return;
261
- await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'open' } }));
286
+ const handleThreadDelete = React.useCallback(async (): Promise<void> => {
287
+ if (annotationId) {
288
+ await dispatch(deleteAnnotationAction(annotationId));
289
+ }
290
+ }, [annotationId, dispatch]);
291
+
292
+ const handleDelete = React.useCallback(
293
+ async (id: string): Promise<void> => {
294
+ if (!annotationId || id === annotationId) return;
295
+ await dispatch(deleteReplyAction({ annotationId, replyId: id }));
262
296
  },
263
297
  [annotationId, dispatch],
264
298
  );
265
299
 
300
+ const handleResolve = React.useCallback(async (): Promise<void> => {
301
+ if (!annotationId) return;
302
+ const result = await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'resolved' } }));
303
+ if (updateAnnotationAction.fulfilled.match(result)) {
304
+ dispatch(setActiveAnnotationIdAction(null));
305
+ }
306
+ }, [annotationId, dispatch]);
307
+
308
+ const handleUnresolve = React.useCallback(async (): Promise<void> => {
309
+ if (!annotationId) return;
310
+ await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'open' } }));
311
+ }, [annotationId, dispatch]);
312
+
266
313
  if (!popupPortalEl) return null;
267
314
 
268
315
  return ReactDOM.createPortal(
@@ -270,45 +317,54 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
270
317
  <div
271
318
  ref={popupRef}
272
319
  aria-label={intl.formatMessage(messages.ariaLabelComment)}
273
- className="ba-PopupV2"
320
+ className="ba-PopupAnnotation"
274
321
  data-resin-component={annotationId ? 'popupThreadV2' : 'popupReplyV2'}
275
- onClick={handleEvent}
322
+ data-resin-feature="annotations"
276
323
  onMouseDown={handleEvent}
277
324
  onMouseMove={handleEvent}
278
- onMouseUp={handleEvent}
279
325
  onTouchMove={handleEvent}
280
326
  onTouchStart={handleEvent}
281
327
  role="presentation"
282
328
  >
283
329
  <BlueprintModernizationProvider enableModernizedComponents>
284
- <MentionContextProvider value={{
285
- collaborationPopoverProps: {
286
- onSubmit: () => Promise.resolve(),
287
- },
288
- }}>
289
- {annotationId ? (
290
- <ThreadedAnnotationsV2
291
- isAnnotations
292
- isResolved={isResolved}
293
- messages={threadMessages}
294
- onAvatarClick={noop}
295
- onDelete={noop}
296
- onPost={handlePost}
297
- onResolve={handleResolve}
298
- onThreadDelete={handleThreadDelete}
299
- onUnresolve={handleUnresolve}
300
- resolvedAt={resolvedAt}
301
- resolvedBy={resolvedBy}
302
- userSelectorProps={userSelectorProps}
303
- />
304
- ) : (
305
- <MessageEditorV2
306
- isFirstAnnotation
307
- onPost={handlePost}
308
- userSelectorProps={userSelectorProps}
309
- />
310
- )}
311
- </MentionContextProvider>
330
+ <TooltipProvider>
331
+ <MentionContextProvider
332
+ value={{
333
+ collaborationPopoverProps: {
334
+ onSubmit: () => Promise.resolve(),
335
+ },
336
+ // Mentions are restricted to file collaborators; the invite popover is intentionally never shown.
337
+ fetchCollaboratorState: async () => true,
338
+ }}
339
+ >
340
+ {annotationId ? (
341
+ <ThreadedAnnotationsV2
342
+ isAnnotations
343
+ isResolved={isResolved}
344
+ isRichTextEnabled={isRichTextEnabled}
345
+ messages={threadMessages}
346
+ onAvatarClick={noop}
347
+ onCopyLink={onCopyLink}
348
+ onDelete={handleDelete}
349
+ onEdit={handleEdit}
350
+ onPost={handlePost}
351
+ onResolve={handleResolve}
352
+ onThreadDelete={handleThreadDelete}
353
+ onUnresolve={handleUnresolve}
354
+ resolvedAt={resolvedAt}
355
+ resolvedBy={resolvedBy}
356
+ userSelectorProps={userSelectorProps}
357
+ />
358
+ ) : (
359
+ <MessageEditorV2
360
+ isFirstAnnotation
361
+ isRichTextEnabled={isRichTextEnabled}
362
+ onPost={handlePost}
363
+ userSelectorProps={userSelectorProps}
364
+ />
365
+ )}
366
+ </MentionContextProvider>
367
+ </TooltipProvider>
312
368
  </BlueprintModernizationProvider>
313
369
  <div data-threaded-annotations-portal />
314
370
  </div>
@@ -70,14 +70,7 @@ const ReplyForm = (props: Props): JSX.Element => {
70
70
  }, [formRef]); // eslint-disable-line react-hooks/exhaustive-deps
71
71
 
72
72
  return (
73
- <Form
74
- ref={formRef}
75
- className="ba-ReplyForm"
76
- data-testid="ba-ReplyForm"
77
- onPointerEnterCapture={undefined}
78
- onPointerLeaveCapture={undefined}
79
- placeholder={undefined}
80
- >
73
+ <Form ref={formRef} className="ba-ReplyForm" data-testid="ba-ReplyForm">
81
74
  <div className="ba-Popup-main">
82
75
  <ReplyField
83
76
  className="ba-Popup-field"
package/src/constants.ts CHANGED
@@ -9,14 +9,13 @@ export const MOUSE_PRIMARY = 1;
9
9
 
10
10
  /**
11
11
  * This is the default location index for video annotations.
12
- * Documents have locations that represent a page but that doesn't apply to
12
+ * Documents have locations that represent a page but that doesn't apply to
13
13
  * videos as they have no pages. The annnotations managers take locations as
14
- * parameters so we need to provide a default value that indicates that this is a
14
+ * parameters so we need to provide a default value that indicates that this is a
15
15
  * video annotation.
16
16
  */
17
17
  export const MEDIA_LOCATION_INDEX = -1;
18
18
 
19
-
20
19
  export enum TARGET_TYPE {
21
20
  FRAME = 'frame',
22
21
  PAGE = 'page',
@@ -97,13 +97,29 @@ export default class DocumentAnnotator extends BaseAnnotator {
97
97
  // Lazily instantiate managers as pages are added or re-rendered
98
98
  if (managers.size === 0) {
99
99
  if (viewMode === 'boundingBoxes') {
100
- managers.add(new BoundingBoxHighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
100
+ managers.add(
101
+ new BoundingBoxHighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }),
102
+ );
101
103
  return managers;
102
104
  }
103
105
 
104
106
  // Annotations mode
105
- managers.add(new PopupManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
106
- managers.add(new DrawingManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
107
+ managers.add(
108
+ new PopupManager({
109
+ location: pageNumber,
110
+ popupPortalEl: this.popupPortalEl,
111
+ referenceEl: pageReferenceEl,
112
+ resinTags,
113
+ }),
114
+ );
115
+ managers.add(
116
+ new DrawingManager({
117
+ location: pageNumber,
118
+ popupPortalEl: this.popupPortalEl,
119
+ referenceEl: pageReferenceEl,
120
+ resinTags,
121
+ }),
122
+ );
107
123
 
108
124
  const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
109
125
 
@@ -118,7 +134,14 @@ export default class DocumentAnnotator extends BaseAnnotator {
118
134
  );
119
135
  }
120
136
 
121
- managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
137
+ managers.add(
138
+ new HighlightManager({
139
+ location: pageNumber,
140
+ popupPortalEl: this.popupPortalEl,
141
+ referenceEl: pageReferenceEl,
142
+ resinTags,
143
+ }),
144
+ );
122
145
  managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
123
146
 
124
147
  const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
@@ -220,10 +243,11 @@ export default class DocumentAnnotator extends BaseAnnotator {
220
243
  width: `${origWidth}px`,
221
244
  });
222
245
  } else {
223
- manager.style({height: '', left: '', top: '', transform: '', width: ''});
246
+ manager.style({ height: '', left: '', top: '', transform: '', width: '' });
224
247
  }
225
248
 
226
249
  manager.render({
250
+ callbacks: { onCopyLink: this.onCopyLink },
227
251
  intl: this.intl,
228
252
  store: this.store,
229
253
  });
@@ -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,11 @@ 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 {
66
+ containerEl: containerEl as HTMLElement,
67
+ containerRect: containerEl.getBoundingClientRect(),
68
+ hasError,
69
+ location,
70
+ range,
71
+ };
65
72
  }
@@ -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,
@@ -86,7 +91,7 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
86
91
  redoDrawingPathGroup();
87
92
  };
88
93
 
89
- const getCurrentLocation = (): number => {
94
+ const getCurrentLocation = (): number => {
90
95
  if (targetType === TARGET_TYPE.FRAME) {
91
96
  return getCurrentVideoLocation();
92
97
  }
@@ -116,12 +121,11 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
116
121
  }
117
122
  }, [drawnPathGroups]);
118
123
 
119
-
120
124
  let annotationsToShow: AnnotationDrawing[] = [];
121
- // For video annotations, we only show the active annotation and we wait for the video
125
+ // For video annotations, we only show the active annotation and we wait for the video
122
126
  // to seek to the annotation location before showing it. This prevents annoations from being
123
127
  // shown on the incorrect frame while the video is seeking.
124
- if (targetType === TARGET_TYPE.FRAME ) {
128
+ if (targetType === TARGET_TYPE.FRAME) {
125
129
  if (!isVideoSeeking && activeAnnotationId) {
126
130
  annotationsToShow = annotations.filter(annotation => annotation.id === activeAnnotationId);
127
131
  }
@@ -129,9 +133,6 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
129
133
  annotationsToShow = annotations;
130
134
  }
131
135
 
132
-
133
-
134
-
135
136
  return (
136
137
  <>
137
138
  <DrawingList
@@ -170,23 +171,31 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
170
171
  color={color}
171
172
  onStart={handleStart}
172
173
  onStop={handleStop}
174
+ rotation={rotation}
173
175
  />
174
176
  )}
175
177
 
176
- {isCreating && hasPathGroups && drawingSVGGroup && canShowPopupToolbar && (
177
- <PopupDrawingToolbar
178
- ref={popupDrawingToolbarRef}
179
- canComment={hasDrawnPathGroups}
180
- canRedo={hasStashedPathGroups}
181
- canUndo={hasDrawnPathGroups}
182
- className={classNames('ba-DrawingAnnotations-toolbar', { 'ba-is-drawing': isDrawing })}
183
- onDelete={handleDelete}
184
- onRedo={handleRedo}
185
- onReply={handleReply}
186
- onUndo={handleUndo}
187
- reference={drawingSVGGroup}
188
- />
189
- )}
178
+ {isCreating &&
179
+ hasPathGroups &&
180
+ drawingSVGGroup &&
181
+ canShowPopupToolbar &&
182
+ (() => {
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
- const { height, width } = creatorEl.getBoundingClientRect();
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] = getPosition(x, y);
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] = getPosition(x, y);
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,5 +1,6 @@
1
1
  const getDrawingCursor = (color: string): string => {
2
- const encodedSvg = encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='32' height='32' viewBox='0 0 32 32'>
2
+ const encodedSvg =
3
+ encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='32' height='32' viewBox='0 0 32 32'>
3
4
  <defs>
4
5
  <path id='ba-DrawingCursor-b' d='M23.0868949,12.6610542 L21.7362465,14.0116939 C21.5985448,14.1493947 21.3758783,14.1493947 21.2381766,14.0116939 L17.9860729,10.7596114 C17.8483713,10.6219106 17.8483713,10.3992455 17.9860729,10.2615447 L19.3367214,8.91090502 C19.8845983,8.36303166 20.7752645,8.36303166 21.3260713,8.91090502 L23.0868949,10.6717173 C23.6377017,11.2195906 23.6377017,12.1102511 23.0868949,12.6610542 Z M16.825863,11.4217471 L9.13214756,19.1154126 L8.51102507,22.6751246 C8.4260602,23.1556124 8.8450249,23.5716446 9.32551588,23.4896101 L12.8852509,22.8655618 L20.5789664,15.1718963 C20.7166681,15.0341955 20.7166681,14.8115304 20.5789664,14.6738296 L17.3268627,11.4217471 C17.1862312,11.2840463 16.9635647,11.2840463 16.825863,11.4217471 Z M12.1352162,18.4562067 C11.974076,18.2950675 11.974076,18.0372447 12.1352162,17.8761055 L16.6471438,13.3642072 C16.8082841,13.203068 17.0661085,13.203068 17.2272488,13.3642072 C17.388389,13.5253464 17.388389,13.7831692 17.2272488,13.9443084 L12.7153212,18.4562067 C12.5541809,18.6173459 12.2963565,18.6173459 12.1352162,18.4562067 L12.1352162,18.4562067 Z M11.0775501,20.9201719 L12.4838652,20.9201719 L12.4838652,21.9836908 L10.5941293,22.3147587 L9.6829543,21.4035896 L10.0140243,19.513866 L11.0775501,19.513866 L11.0775501,20.9201719 Z'/>
5
6
  <filter id='ba-DrawingCursor-a' width='196.7%' height='196.7%' x='-48.3%' y='-41.7%' filterUnits='objectBoundingBox'>