box-annotations 5.3.1-beta.0 → 5.3.1-beta.2

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 (71) hide show
  1. package/dist/annotations.css +1 -1
  2. package/dist/annotations.js +1 -1
  3. package/i18n/bn-IN.js +24 -23
  4. package/i18n/da-DK.js +24 -23
  5. package/i18n/de-DE.js +24 -23
  6. package/i18n/en-AU.js +4 -23
  7. package/i18n/en-CA.js +2 -24
  8. package/i18n/en-GB.js +4 -23
  9. package/i18n/en-US.js +4 -24
  10. package/i18n/en-x-pseudo.js +6 -0
  11. package/i18n/es-419.js +24 -23
  12. package/i18n/es-ES.js +24 -23
  13. package/i18n/fi-FI.js +24 -23
  14. package/i18n/fr-CA.js +24 -23
  15. package/i18n/fr-FR.js +24 -23
  16. package/i18n/hi-IN.js +24 -23
  17. package/i18n/it-IT.js +24 -23
  18. package/i18n/ja-JP.js +24 -23
  19. package/i18n/ko-KR.js +24 -23
  20. package/i18n/nb-NO.js +24 -23
  21. package/i18n/nl-NL.js +24 -23
  22. package/i18n/pl-PL.js +24 -23
  23. package/i18n/pt-BR.js +24 -23
  24. package/i18n/ru-RU.js +24 -23
  25. package/i18n/sv-SE.js +24 -23
  26. package/i18n/tr-TR.js +24 -23
  27. package/i18n/zh-CN.js +24 -23
  28. package/i18n/zh-TW.js +24 -23
  29. package/package.json +8 -8
  30. package/src/api/types.ts +5 -0
  31. package/src/common/BaseAnnotator.ts +9 -0
  32. package/src/common/EventEmitter.ts +11 -3
  33. package/src/components/Popups/PopupV2.tsx +32 -1
  34. package/src/components/ReplyForm/ReplyFormContainer.tsx +7 -4
  35. package/src/document/DocumentAnnotator.ts +28 -11
  36. package/src/document/docUtil.ts +5 -1
  37. package/src/popup/PopupLayer.tsx +6 -1
  38. package/src/region/RegionCreation.tsx +4 -3
  39. package/src/region/RegionCreationContainer.tsx +3 -0
  40. package/src/store/annotations/actions.ts +32 -6
  41. package/src/store/annotations/reducer.ts +2 -0
  42. package/src/store/options/selectors.ts +5 -0
  43. package/i18n/bn-IN.properties +0 -44
  44. package/i18n/da-DK.properties +0 -44
  45. package/i18n/de-DE.properties +0 -44
  46. package/i18n/en-AU.properties +0 -44
  47. package/i18n/en-CA.properties +0 -44
  48. package/i18n/en-GB.properties +0 -44
  49. package/i18n/en-US.properties +0 -44
  50. package/i18n/en-x-pseudo.properties +0 -32
  51. package/i18n/es-419.properties +0 -44
  52. package/i18n/es-ES.properties +0 -44
  53. package/i18n/fi-FI.properties +0 -44
  54. package/i18n/fr-CA.properties +0 -44
  55. package/i18n/fr-FR.properties +0 -44
  56. package/i18n/hi-IN.properties +0 -44
  57. package/i18n/it-IT.properties +0 -44
  58. package/i18n/ja-JP.properties +0 -44
  59. package/i18n/json/src/components/BoundingBoxHighlight/messages.json +0 -1
  60. package/i18n/json/src/components/Popups/messages.json +0 -1
  61. package/i18n/json/src/messages.json +0 -1
  62. package/i18n/ko-KR.properties +0 -44
  63. package/i18n/nb-NO.properties +0 -44
  64. package/i18n/nl-NL.properties +0 -44
  65. package/i18n/pl-PL.properties +0 -44
  66. package/i18n/pt-BR.properties +0 -44
  67. package/i18n/ru-RU.properties +0 -44
  68. package/i18n/sv-SE.properties +0 -44
  69. package/i18n/tr-TR.properties +0 -44
  70. package/i18n/zh-CN.properties +0 -44
  71. package/i18n/zh-TW.properties +0 -44
package/i18n/zh-TW.js CHANGED
@@ -1,24 +1,25 @@
1
+ // THIS IS A GENERATED FILE. DO NOT EDIT MANUALLY OR YOUR CHANGES WILL BE OVERWRITTEN
1
2
  export default {
2
- "ba.annotationsClose": "關閉",
3
- "ba.annotationsCreateError": "很抱歉,無法建立註解。",
4
- "ba.annotationsLoadError": "很抱歉,無法載入此檔案的註解。",
5
- "ba.annotationsPost": "張貼",
6
- "ba.annotationsSave": "儲存",
7
- "ba.boundingBoxHighlight.viewNextReference": "檢視下一個參考項目",
8
- "ba.boundingBoxHighlight.viewPrevReference": "檢視先前的參考項目",
9
- "ba.popup.reply.comment": "留言",
10
- "ba.popup.reply.field": "鍵入留言",
11
- "ba.popup.reply.mentionLoading": "正在載入使用者…",
12
- "ba.popup.reply.mentionSelector": "提及共同作業者",
13
- "ba.popups.cancel": "取消",
14
- "ba.popups.drawing.addComment": "新增留言",
15
- "ba.popups.drawing.delete": "刪除",
16
- "ba.popups.drawing.redo": "重做",
17
- "ba.popups.drawing.undo": "復原",
18
- "ba.popups.popupCursor.regionPrompt": "繪圖一個方塊以留言",
19
- "ba.popups.popupHighlight.promoter": "醒目提示和留言",
20
- "ba.popups.popupHighlight.restrictedPrompt": "留言限制在單一頁面",
21
- "ba.popups.popupList.prompt": "提及某人來通知該名人員",
22
- "ba.popups.post": "發佈",
23
- "ba.popups.replyField.placeholder": "輸入留言..."
24
- }
3
+ "ba.annotationsClose": "關閉",
4
+ "ba.annotationsCreateError": "很抱歉,無法建立註解。",
5
+ "ba.annotationsLoadError": "很抱歉,無法載入此檔案的註解。",
6
+ "ba.annotationsPost": "張貼",
7
+ "ba.annotationsSave": "儲存",
8
+ "ba.boundingBoxHighlight.viewNextReference": "檢視下一個參考項目",
9
+ "ba.boundingBoxHighlight.viewPrevReference": "檢視先前的參考項目",
10
+ "ba.popup.reply.comment": "留言",
11
+ "ba.popup.reply.field": "鍵入留言",
12
+ "ba.popup.reply.mentionLoading": "正在載入使用者...",
13
+ "ba.popup.reply.mentionSelector": "提及共同作業者",
14
+ "ba.popups.cancel": "取消",
15
+ "ba.popups.drawing.addComment": "新增留言",
16
+ "ba.popups.drawing.delete": "刪除",
17
+ "ba.popups.drawing.redo": "重做",
18
+ "ba.popups.drawing.undo": "復原",
19
+ "ba.popups.popupCursor.regionPrompt": "繪圖一個方塊以留言",
20
+ "ba.popups.popupHighlight.promoter": "醒目提示和留言",
21
+ "ba.popups.popupHighlight.restrictedPrompt": "留言限制在單一頁面",
22
+ "ba.popups.popupList.prompt": "提及某人以進行通知",
23
+ "ba.popups.post": "張貼",
24
+ "ba.popups.replyField.placeholder": "輸入留言..."
25
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "box-annotations",
3
- "version": "5.3.1-beta.0",
3
+ "version": "5.3.1-beta.2",
4
4
  "description": "Box Annotations",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -40,12 +40,13 @@
40
40
  "@box/item-icon": "^3.2.0",
41
41
  "@box/languages": "^1.1.0",
42
42
  "@box/readable-time": "^2.1.32",
43
- "@box/threaded-annotations": "^4.11.0",
43
+ "@box/threaded-annotations": "^4.14.0",
44
44
  "@box/types": "^2.1.8",
45
45
  "@box/user-selector": "^2.1.35",
46
46
  "@cfaester/enzyme-adapter-react-18": "^0.8.0",
47
47
  "@commitlint/cli": "^19.8.0",
48
48
  "@commitlint/config-conventional": "^19.8.0",
49
+ "@formatjs/cli": "^6.16.26",
49
50
  "@formatjs/intl-pluralrules": "^1.5.5",
50
51
  "@formatjs/intl-relativetimeformat": "^4.5.12",
51
52
  "@popperjs/core": "^2.4.0",
@@ -69,9 +70,8 @@
69
70
  "babel-jest": "^29.7.0",
70
71
  "babel-loader": "^9.2.1",
71
72
  "babel-plugin-istanbul": "^6.0.0",
72
- "babel-plugin-react-intl": "^7.5.1",
73
73
  "babel-plugin-transform-require-ignore": "^0.1.1",
74
- "box-ui-elements": "^20.0.0",
74
+ "box-ui-elements": "^28.0.0-beta.2",
75
75
  "circular-dependency-plugin": "^5.2.0",
76
76
  "classnames": "^2.2.5",
77
77
  "clsx": "^2.1.1",
@@ -142,17 +142,17 @@
142
142
  "@box/combobox-with-api": "^1.42.22",
143
143
  "@box/item-icon": "^3.2.0",
144
144
  "@box/readable-time": "^2.1.32",
145
- "@box/threaded-annotations": "^4.11.0",
145
+ "@box/threaded-annotations": "^4.14.0",
146
146
  "@box/types": "^2.1.8",
147
147
  "@box/user-selector": "^2.1.35",
148
148
  "axios": "^0.33.0"
149
149
  },
150
150
  "scripts": {
151
151
  "build": "yarn setup && yarn build:prod:dist",
152
- "build:i18n": "props2es",
153
152
  "build:dev:dist": "LANGUAGE=en-US BABEL_ENV=dev NODE_ENV=dev webpack --config scripts/webpack.config.js --mode development --progress",
154
153
  "build:prod:dist": "LANGUAGE=en-US BABEL_ENV=production NODE_ENV=production webpack --config scripts/webpack.config.js --mode production",
155
- "clean": "rm -rf dist reports/* i18n/json i18n/*.js",
154
+ "clean": "rm -rf dist reports/* i18n/json",
155
+ "extract:i18n": "mkdir -p i18n/json && formatjs extract 'src/**/*.{js,ts,tsx}' --ignore='src/**/__tests__/**' --ignore='src/**/__mocks__/**' --out-file i18n/json/en-US.json",
156
156
  "cy:open": "yarn cy:wait; yarn cypress open",
157
157
  "cy:run": "yarn cy:wait; yarn cypress run --spec test/integration/**/*.test.js",
158
158
  "cy:wait": "wait-on http-get://127.0.0.1:8001",
@@ -166,7 +166,7 @@
166
166
  "release:minor": "scripts/release.sh -n",
167
167
  "release:patch": "scripts/release.sh -p",
168
168
  "release:pre": "scripts/release.sh -x",
169
- "setup": "yarn install && yarn clean && yarn build:i18n",
169
+ "setup": "yarn install && yarn clean",
170
170
  "start": "yarn setup && yarn build:dev:dist --watch",
171
171
  "start:dev": "yarn setup && LANGUAGE=en-US BABEL_ENV=dev NODE_ENV=dev yarn webpack-dev-server --config scripts/webpack.config.js --mode development",
172
172
  "start:linked": "IS_LINKED=1 yarn start",
package/src/api/types.ts CHANGED
@@ -31,6 +31,7 @@ export interface AnnotationsAPI {
31
31
  permissions: Permissions,
32
32
  successCallback: (result: Annotation) => void,
33
33
  errorCallback: (error: APIError) => void,
34
+ shouldEnableRichText?: boolean,
34
35
  ): Promise<void>;
35
36
 
36
37
  createAnnotationReply(
@@ -40,6 +41,7 @@ export interface AnnotationsAPI {
40
41
  message: string,
41
42
  successCallback: (result: Reply) => void,
42
43
  errorCallback: (error: APIError) => void,
44
+ shouldEnableRichText?: boolean,
43
45
  ): Promise<void>;
44
46
 
45
47
  deleteAnnotation(
@@ -59,6 +61,7 @@ export interface AnnotationsAPI {
59
61
  limit?: number,
60
62
  shouldFetchAll?: boolean,
61
63
  shouldFetchReplies?: boolean,
64
+ shouldEnableRichText?: boolean,
62
65
  ): Promise<void>;
63
66
 
64
67
  updateAnnotation(
@@ -68,6 +71,7 @@ export interface AnnotationsAPI {
68
71
  payload: { message?: string; status?: string },
69
72
  successCallback: (result: Annotation) => void,
70
73
  errorCallback: (error: APIError) => void,
74
+ shouldEnableRichText?: boolean,
71
75
  ): Promise<void>;
72
76
 
73
77
  destroy(): void;
@@ -88,6 +92,7 @@ export interface ThreadedCommentsAPI {
88
92
  fileId: string | null;
89
93
  message?: string;
90
94
  permissions: ReplyPermissions;
95
+ shouldEnableRichText?: boolean;
91
96
  status?: string;
92
97
  successCallback: (reply: Reply) => void;
93
98
  }): void;
@@ -45,6 +45,8 @@ export type Options = {
45
45
  intl: IntlOptions;
46
46
  locale?: string;
47
47
  onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
48
+ // When false, skip annotation create. Existing annotations still render.
49
+ showAnnotationsControls?: boolean;
48
50
  token: Token;
49
51
  };
50
52
 
@@ -339,6 +341,13 @@ export default class BaseAnnotator extends EventEmitter {
339
341
  };
340
342
 
341
343
  protected handleSetActive = (annotationId: string | null): void => {
344
+ const state = this.store.getState();
345
+
346
+ // Ignore ids this pane does not own. Clear locally so the host is not told to deselect.
347
+ if (annotationId && state?.annotations?.isInitialized && !store.getAnnotation(state, annotationId)) {
348
+ this.store.dispatch(store.clearActiveAnnotationIdAction());
349
+ return;
350
+ }
342
351
  this.setActiveId(annotationId);
343
352
  };
344
353
 
@@ -1,7 +1,12 @@
1
1
  import eventManager from './EventManager';
2
2
 
3
+ type Listener = (...args: any[]) => void;
4
+
3
5
  export default class EventEmitter {
4
- addListener(event: string | symbol, listener: (...args: any[]) => void): void {
6
+ private ownListeners: Array<{ event: string | symbol; listener: Listener }> = [];
7
+
8
+ addListener(event: string | symbol, listener: Listener): void {
9
+ this.ownListeners.push({ event, listener });
5
10
  eventManager.addListener(event, listener);
6
11
  }
7
12
 
@@ -10,10 +15,13 @@ export default class EventEmitter {
10
15
  }
11
16
 
12
17
  removeAllListeners(): void {
13
- eventManager.removeAllListeners();
18
+ // Shared eventManager: remove only this instance's listeners.
19
+ this.ownListeners.forEach(({ event, listener }) => eventManager.removeListener(event, listener));
20
+ this.ownListeners = [];
14
21
  }
15
22
 
16
- removeListener(event: string | symbol, listener: (...args: any[]) => void): void {
23
+ removeListener(event: string | symbol, listener: Listener): void {
24
+ this.ownListeners = this.ownListeners.filter(own => !(own.event === event && own.listener === listener));
17
25
  eventManager.removeListener(event, listener);
18
26
  }
19
27
  }
@@ -19,6 +19,7 @@ import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
19
19
 
20
20
  import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
21
21
  import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext';
22
+ import useOutsideEvent from '../../common/useOutsideEvent';
22
23
  import {
23
24
  createReplyAction,
24
25
  deleteAnnotationAction,
@@ -41,6 +42,7 @@ import './PopupV2.scss';
41
42
 
42
43
  export type Props = {
43
44
  annotationId?: string;
45
+ onCancel?: () => void;
44
46
  onSubmit: (text: string) => void;
45
47
  popupPortalEl?: HTMLElement | null;
46
48
  reference: PopupReference;
@@ -70,6 +72,15 @@ const createDocumentNode = (content: JSONContent | null): DocumentNodeV2 => {
70
72
  const serializePopupMessage = (doc: DocumentNodeV2, isRichTextEnabled: boolean): string =>
71
73
  isRichTextEnabled ? serializeMessageToMarkdown(doc) : serializeMentionMarkup(doc).text;
72
74
 
75
+ const isEmptyEditorContent = (content: JSONContent | null, isRichTextEnabled: boolean): boolean => {
76
+ if (!content) {
77
+ return true;
78
+ }
79
+
80
+ const text = serializePopupMessage(createDocumentNode(content), isRichTextEnabled);
81
+ return text.trim().length === 0;
82
+ };
83
+
73
84
  const literalToString = (literal: TokenLiteral): string | null => {
74
85
  if (!literal) return null;
75
86
  if (typeof literal === 'string') return literal;
@@ -105,13 +116,14 @@ const fetchAvatarBlob = async (
105
116
  }
106
117
  };
107
118
 
108
- const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
119
+ const PopupV2 = ({ annotationId, onCancel, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
109
120
  const intl = useIntl();
110
121
  const dispatch = useDispatch<AppThunkDispatch>();
111
122
  const { onCopyLink: consumerOnCopyLink } = React.useContext(AnnotationCallbacksContext);
112
123
  const popupRef = React.useRef<HTMLDivElement>(null);
113
124
  const popperRef = React.useRef<Instance>();
114
125
  const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
126
+ const hasTextRef = React.useRef(false);
115
127
 
116
128
  const apiHost = useSelector(getApiHost);
117
129
  const fileId = useSelector(getFileId);
@@ -185,6 +197,21 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
185
197
  event.stopPropagation();
186
198
  }, []);
187
199
 
200
+ const handleEditorChange = React.useCallback(
201
+ (content: JSONContent | null): void => {
202
+ hasTextRef.current = !isEmptyEditorContent(content, isRichTextEnabled);
203
+ },
204
+ [isRichTextEnabled],
205
+ );
206
+
207
+ const handleClickAway = React.useCallback((): void => {
208
+ if (!hasTextRef.current) {
209
+ onCancel?.();
210
+ }
211
+ }, [onCancel]);
212
+
213
+ useOutsideEvent('mousedown', popupRef, onCancel ? handleClickAway : undefined);
214
+
188
215
  React.useEffect(() => {
189
216
  if (!annotation) return undefined;
190
217
 
@@ -310,6 +337,9 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
310
337
  await dispatch(updateAnnotationAction({ annotationId, payload: { status: 'open' } }));
311
338
  }, [annotationId, dispatch]);
312
339
 
340
+ // No messages means this pane does not own the thread; do not render an empty composer.
341
+ if (annotationId && !threadMessages.length) return null;
342
+
313
343
  if (!popupPortalEl) return null;
314
344
 
315
345
  return ReactDOM.createPortal(
@@ -359,6 +389,7 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
359
389
  <MessageEditorV2
360
390
  isFirstAnnotation
361
391
  isRichTextEnabled={isRichTextEnabled}
392
+ onChange={handleEditorChange}
362
393
  onPost={handlePost}
363
394
  userSelectorProps={userSelectorProps}
364
395
  />
@@ -1,4 +1,4 @@
1
- import createMentionSelectorState from 'box-ui-elements/es/components/form-elements/draft-js-mention-selector/createMentionSelectorState';
1
+ import createMentionTimestampSelectorState from 'box-ui-elements/es/components/form-elements/draft-js-mention-selector/createMentionTimestampSelectorState';
2
2
  import { getFormattedCommentText } from 'box-ui-elements/es/components/form-elements/draft-js-mention-selector/utils';
3
3
  import { EditorState, SelectionState } from 'draft-js';
4
4
  import { FormikBag, withFormik } from 'formik';
@@ -30,13 +30,16 @@ export const mapStateToProps = (state: AppState): PropsFromState => ({
30
30
  export const mapPropsToErrors = (): FormErrors => ({ editorState: 'initial' });
31
31
 
32
32
  export const mapPropsToValues = ({ cursorPosition: prevCursorPosition, value = '' }: Props): FormValues => {
33
- const mentionState = withMentionDecorator(createMentionSelectorState(value));
34
- const cursorPosition = value ? prevCursorPosition : 0;
33
+ const mentionState = withMentionDecorator(createMentionTimestampSelectorState(value));
34
+ const selection = mentionState.getSelection();
35
+ const blockLength = mentionState.getCurrentContent().getBlockForKey(selection.getAnchorKey()).getLength();
36
+ // The saved message is trimmed, so the saved cursor can sit past the end of the rebuilt text
37
+ const cursorPosition = value ? Math.min(prevCursorPosition, blockLength) : 0;
35
38
 
36
39
  return {
37
40
  editorState: EditorState.forceSelection(
38
41
  mentionState,
39
- mentionState.getSelection().merge({
42
+ selection.merge({
40
43
  anchorOffset: cursorPosition,
41
44
  focusOffset: cursorPosition,
42
45
  hasFocus: true,
@@ -32,6 +32,8 @@ export default class DocumentAnnotator extends BaseAnnotator {
32
32
 
33
33
  highlightListener?: HighlightListener;
34
34
 
35
+ private readonly canCreate: boolean;
36
+
35
37
  managers: Map<number, Set<Manager>> = new Map();
36
38
 
37
39
  /** Tracks which view mode the current managers were created for. Used to destroy/recreate when switching. */
@@ -40,11 +42,24 @@ export default class DocumentAnnotator extends BaseAnnotator {
40
42
  constructor(options: Options) {
41
43
  super(options);
42
44
 
43
- this.highlightListener = new HighlightListener({ getSelection, store: this.store });
45
+ // Controls off: skip create; existing annotations still render.
46
+ this.canCreate = options.showAnnotationsControls !== false;
47
+ if (this.canCreate) {
48
+ this.highlightListener = new HighlightListener({
49
+ getSelection: this.getDocumentSelection,
50
+ store: this.store,
51
+ });
52
+ }
44
53
 
45
54
  this.addListener(Event.ANNOTATIONS_MODE_CHANGE, this.handleChangeMode);
46
55
  }
47
56
 
57
+ // Resolve .bp-doc at event time; it may not exist in the constructor.
58
+ getDocumentSelection = (): ReturnType<typeof getSelection> => {
59
+ const rootEl = this.annotatedEl ?? this.getAnnotatedElement();
60
+ return rootEl ? getSelection(rootEl) : null;
61
+ };
62
+
48
63
  destroy(): void {
49
64
  this.removeListener(Event.ANNOTATIONS_MODE_CHANGE, this.handleChangeMode);
50
65
 
@@ -123,10 +138,10 @@ export default class DocumentAnnotator extends BaseAnnotator {
123
138
 
124
139
  const textLayer = pageEl.querySelector('.textLayer') as HTMLElement;
125
140
 
126
- if (textLayer) {
141
+ if (this.canCreate && textLayer) {
127
142
  managers.add(
128
143
  new HighlightCreatorManager({
129
- getSelection,
144
+ getSelection: this.getDocumentSelection,
130
145
  referenceEl: textLayer,
131
146
  selectionChangeDelay: TEXT_LAYER_ENHANCEMENT,
132
147
  store: this.store,
@@ -144,15 +159,17 @@ export default class DocumentAnnotator extends BaseAnnotator {
144
159
  );
145
160
  managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
146
161
 
147
- const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
162
+ if (this.canCreate) {
163
+ const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
148
164
 
149
- managers.add(
150
- new RegionCreationManager({
151
- location: pageNumber,
152
- referenceEl: canvasLayerEl || pageReferenceEl,
153
- resinTags,
154
- }),
155
- );
165
+ managers.add(
166
+ new RegionCreationManager({
167
+ location: pageNumber,
168
+ referenceEl: canvasLayerEl || pageReferenceEl,
169
+ resinTags,
170
+ }),
171
+ );
172
+ }
156
173
  }
157
174
 
158
175
  return managers;
@@ -29,13 +29,17 @@ export function getPageNumber(element: Element | null): number | undefined {
29
29
  return pageNumber ? parseInt(pageNumber, 10) : undefined;
30
30
  }
31
31
 
32
- export function getSelection(): Selection | null {
32
+ export function getSelection(rootEl?: HTMLElement | null): Selection | null {
33
33
  const selection = window.getSelection();
34
34
 
35
35
  if (!selection || selection.isCollapsed || !selection.rangeCount || !selection.focusNode) {
36
36
  return null;
37
37
  }
38
38
 
39
+ if (rootEl && !rootEl.contains(selection.focusNode)) {
40
+ return null;
41
+ }
42
+
39
43
  // Firefox allows to select multiple ranges in the document by using Ctrl/Cmd+click
40
44
  // We only care about the last range
41
45
  let range = selection.getRangeAt(selection.rangeCount - 1);
@@ -140,7 +140,12 @@ const PopupLayer = (props: Props): JSX.Element | null => {
140
140
  if (showCreator && isThreadedAnnotation) {
141
141
  return (
142
142
  <div className="ba-PopupLayer-popup">
143
- <PopupV2 onSubmit={handleSubmit} popupPortalEl={popupPortalEl} reference={reference} />
143
+ <PopupV2
144
+ onCancel={handleCancel}
145
+ onSubmit={handleSubmit}
146
+ popupPortalEl={popupPortalEl}
147
+ reference={reference}
148
+ />
144
149
  </div>
145
150
  );
146
151
  }
@@ -8,6 +8,7 @@ import { TARGET_TYPE } from '../constants';
8
8
  import { getVideoCurrentTimeInMilliseconds } from '../utils/useVideoTiming';
9
9
 
10
10
  type Props = {
11
+ hasStagedItem?: boolean;
11
12
  isCreating: boolean;
12
13
  location: number;
13
14
  resetCreator: () => void;
@@ -33,10 +34,10 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
33
34
  state: State = {};
34
35
 
35
36
  handleAbort = (): void => {
36
- const { resetCreator, staged } = this.props;
37
+ const { hasStagedItem, resetCreator, staged } = this.props;
37
38
 
38
- // Click-away (a click too small to start a new region) must not discard an in-progress comment/annotation
39
- if (staged) {
39
+ // Click-away must not discard any in-progress comment/annotation
40
+ if (staged || hasStagedItem) {
40
41
  return;
41
42
  }
42
43
 
@@ -4,6 +4,7 @@ import {
4
4
  CreatorItemRegion,
5
5
  CreatorStatus,
6
6
  getAnnotationMode,
7
+ getCreatorStaged,
7
8
  getCreatorStagedForLocation,
8
9
  getCreatorStatus,
9
10
  getRotation,
@@ -18,6 +19,7 @@ import RegionCreation from './RegionCreation';
18
19
  import withProviders from '../common/withProviders';
19
20
 
20
21
  export type Props = {
22
+ hasStagedItem: boolean;
21
23
  isCreating: boolean;
22
24
  rotation: number;
23
25
  staged: CreatorItemRegion | null;
@@ -27,6 +29,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
27
29
  const staged = getCreatorStagedForLocation(state, location);
28
30
 
29
31
  return {
32
+ hasStagedItem: Boolean(getCreatorStaged(state)),
30
33
  isCreating: getAnnotationMode(state) === Mode.REGION && getCreatorStatus(state) !== CreatorStatus.pending,
31
34
  rotation: getRotation(state),
32
35
  staged: isCreatorStagedRegion(staged) ? staged : null,
@@ -3,7 +3,7 @@ import { Annotation, NewAnnotation, Reply } from '../../@types';
3
3
  import { APICollection } from '../../api';
4
4
  import { AppThunkAPI } from '../types';
5
5
  import { getAnnotation } from './selectors';
6
- import { getFileId, getFileVersionId, getPermissions, isFeatureEnabled } from '../options';
6
+ import { getFileId, getFileVersionId, getIsRichTextEnabled, getPermissions, isFeatureEnabled } from '../options';
7
7
 
8
8
  export const createAnnotationAction = createAsyncThunk<Annotation, NewAnnotation, AppThunkAPI>(
9
9
  'CREATE_ANNOTATION',
@@ -14,13 +14,22 @@ export const createAnnotationAction = createAsyncThunk<Annotation, NewAnnotation
14
14
  const fileId = getFileId(state);
15
15
  const fileVersionId = getFileVersionId(state);
16
16
  const permissions = getPermissions(state);
17
+ const shouldEnableRichText = getIsRichTextEnabled(state);
17
18
 
18
19
  signal.addEventListener('abort', () => {
19
20
  client.destroy();
20
21
  });
21
22
 
22
23
  return new Promise<Annotation>((resolve, reject) => {
23
- client.createAnnotation(fileId, fileVersionId, newAnnotation, permissions, resolve, reject);
24
+ client.createAnnotation(
25
+ fileId,
26
+ fileVersionId,
27
+ newAnnotation,
28
+ permissions,
29
+ resolve,
30
+ reject,
31
+ shouldEnableRichText,
32
+ );
24
33
  });
25
34
  },
26
35
  );
@@ -40,11 +49,22 @@ export const fetchAnnotationsAction = createAsyncThunk<APICollection<Annotation>
40
49
  client.destroy();
41
50
  });
42
51
 
43
- const shouldFetchReplies = isFeatureEnabled({ options: getState().options }, 'isThreadedAnnotation');
52
+ const shouldFetchReplies = isFeatureEnabled(state, 'isThreadedAnnotation');
53
+ const shouldEnableRichText = getIsRichTextEnabled(state);
44
54
 
45
55
  // Wrap the client request in a promise to allow it to be returned and cancelled
46
56
  return new Promise<APICollection<Annotation>>((resolve, reject) => {
47
- client.getAnnotations(fileId, fileVersionId, permissions, resolve, reject, 1000, false, shouldFetchReplies);
57
+ client.getAnnotations(
58
+ fileId,
59
+ fileVersionId,
60
+ permissions,
61
+ resolve,
62
+ reject,
63
+ 1000,
64
+ false,
65
+ shouldFetchReplies,
66
+ shouldEnableRichText,
67
+ );
48
68
  });
49
69
  },
50
70
  );
@@ -60,13 +80,14 @@ export const createReplyAction = createAsyncThunk<
60
80
  const filePermissions = getPermissions(state);
61
81
  const annotation = getAnnotation(state, annotationId);
62
82
  const permissions = { ...filePermissions, ...annotation?.permissions };
83
+ const shouldEnableRichText = getIsRichTextEnabled(state);
63
84
 
64
85
  signal.addEventListener('abort', () => {
65
86
  client.destroy();
66
87
  });
67
88
 
68
89
  const reply = await new Promise<Reply>((resolve, reject) => {
69
- client.createAnnotationReply(fileId, annotationId, permissions, message, resolve, reject);
90
+ client.createAnnotationReply(fileId, annotationId, permissions, message, resolve, reject, shouldEnableRichText);
70
91
  });
71
92
 
72
93
  return { annotationId, reply };
@@ -105,13 +126,14 @@ export const updateAnnotationAction = createAsyncThunk<
105
126
  const filePermissions = getPermissions(state);
106
127
  const existingAnnotation = getAnnotation(state, annotationId);
107
128
  const permissions = { ...filePermissions, ...existingAnnotation?.permissions };
129
+ const shouldEnableRichText = getIsRichTextEnabled(state);
108
130
 
109
131
  signal.addEventListener('abort', () => {
110
132
  client.destroy();
111
133
  });
112
134
 
113
135
  return new Promise<Annotation>((resolve, reject) => {
114
- client.updateAnnotation(fileId, annotationId, permissions, payload, resolve, reject);
136
+ client.updateAnnotation(fileId, annotationId, permissions, payload, resolve, reject, shouldEnableRichText);
115
137
  });
116
138
  });
117
139
 
@@ -125,6 +147,7 @@ export const updateReplyAction = createAsyncThunk<
125
147
  const fileId = getFileId(state);
126
148
  const annotation = getAnnotation(state, annotationId);
127
149
  const reply = annotation?.replies?.find(r => r.id === replyId);
150
+ const shouldEnableRichText = getIsRichTextEnabled(state);
128
151
 
129
152
  if (!reply) {
130
153
  throw new Error(`updateReplyAction: reply ${replyId} not found on annotation ${annotationId}`);
@@ -141,6 +164,7 @@ export const updateReplyAction = createAsyncThunk<
141
164
  fileId,
142
165
  message: payload.message,
143
166
  permissions: reply.permissions ?? {},
167
+ shouldEnableRichText,
144
168
  status: payload.status,
145
169
  successCallback: resolve,
146
170
  });
@@ -194,4 +218,6 @@ export const applySidebarReplyUpdateAction = createAction<SidebarReplyMutationPa
194
218
 
195
219
  export const removeAnnotationAction = createAction<string>('REMOVE_ANNOTATION');
196
220
  export const setActiveAnnotationIdAction = createAction<string | null>('SET_ACTIVE_ANNOTATION_ID');
221
+ // Clears activeId without emitting ACTIVE_CHANGE.
222
+ export const clearActiveAnnotationIdAction = createAction('CLEAR_ACTIVE_ANNOTATION_ID');
197
223
  export const setIsInitialized = createAction('SET_IS_INITIALIZED');
@@ -6,6 +6,7 @@ import {
6
6
  applySidebarReplyCreateAction,
7
7
  applySidebarReplyDeleteAction,
8
8
  applySidebarReplyUpdateAction,
9
+ clearActiveAnnotationIdAction,
9
10
  createAnnotationAction,
10
11
  createReplyAction,
11
12
  deleteAnnotationAction,
@@ -22,6 +23,7 @@ import { setViewModeAction } from '../options/actions';
22
23
  const activeAnnotationId = createReducer<AnnotationsState['activeId']>(null, builder =>
23
24
  builder
24
25
  /* Preview will set the active ID with a an event which will trigger the url change */
26
+ .addCase(clearActiveAnnotationIdAction, () => null)
25
27
  .addCase(createAnnotationAction.fulfilled, () => null)
26
28
  .addCase(deleteAnnotationAction.fulfilled, (state, { payload: id }) => (state === id ? null : state))
27
29
  .addCase(removeAnnotationAction, (state, { payload: id }) => (state === id ? null : state))
@@ -18,3 +18,8 @@ export const getScale = (state: State): number => state.options.scale;
18
18
  export const getToken = (state: State): Token => state.options.token;
19
19
  export const isFeatureEnabled = (state: State, featurename: string): boolean =>
20
20
  getProp(getFeatures(state), featurename, false);
21
+
22
+ // Rich text is only rendered and authored in the threaded popup (PopupV2). The legacy popup editor
23
+ // is plain text, so the flag only applies when threaded annotations are on.
24
+ export const getIsRichTextEnabled = (state: State): boolean =>
25
+ isFeatureEnabled(state, 'isThreadedAnnotation') && isFeatureEnabled(state, 'isRichTextEnabled');