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.
- package/dist/annotations.css +1 -1
- package/dist/annotations.js +1 -1
- package/i18n/bn-IN.js +24 -23
- package/i18n/da-DK.js +24 -23
- package/i18n/de-DE.js +24 -23
- package/i18n/en-AU.js +4 -23
- package/i18n/en-CA.js +2 -24
- package/i18n/en-GB.js +4 -23
- package/i18n/en-US.js +4 -24
- package/i18n/en-x-pseudo.js +6 -0
- package/i18n/es-419.js +24 -23
- package/i18n/es-ES.js +24 -23
- package/i18n/fi-FI.js +24 -23
- package/i18n/fr-CA.js +24 -23
- package/i18n/fr-FR.js +24 -23
- package/i18n/hi-IN.js +24 -23
- package/i18n/it-IT.js +24 -23
- package/i18n/ja-JP.js +24 -23
- package/i18n/ko-KR.js +24 -23
- package/i18n/nb-NO.js +24 -23
- package/i18n/nl-NL.js +24 -23
- package/i18n/pl-PL.js +24 -23
- package/i18n/pt-BR.js +24 -23
- package/i18n/ru-RU.js +24 -23
- package/i18n/sv-SE.js +24 -23
- package/i18n/tr-TR.js +24 -23
- package/i18n/zh-CN.js +24 -23
- package/i18n/zh-TW.js +24 -23
- package/package.json +8 -8
- package/src/api/types.ts +5 -0
- package/src/common/BaseAnnotator.ts +9 -0
- package/src/common/EventEmitter.ts +11 -3
- package/src/components/Popups/PopupV2.tsx +32 -1
- package/src/components/ReplyForm/ReplyFormContainer.tsx +7 -4
- package/src/document/DocumentAnnotator.ts +28 -11
- package/src/document/docUtil.ts +5 -1
- package/src/popup/PopupLayer.tsx +6 -1
- package/src/region/RegionCreation.tsx +4 -3
- package/src/region/RegionCreationContainer.tsx +3 -0
- package/src/store/annotations/actions.ts +32 -6
- package/src/store/annotations/reducer.ts +2 -0
- package/src/store/options/selectors.ts +5 -0
- package/i18n/bn-IN.properties +0 -44
- package/i18n/da-DK.properties +0 -44
- package/i18n/de-DE.properties +0 -44
- package/i18n/en-AU.properties +0 -44
- package/i18n/en-CA.properties +0 -44
- package/i18n/en-GB.properties +0 -44
- package/i18n/en-US.properties +0 -44
- package/i18n/en-x-pseudo.properties +0 -32
- package/i18n/es-419.properties +0 -44
- package/i18n/es-ES.properties +0 -44
- package/i18n/fi-FI.properties +0 -44
- package/i18n/fr-CA.properties +0 -44
- package/i18n/fr-FR.properties +0 -44
- package/i18n/hi-IN.properties +0 -44
- package/i18n/it-IT.properties +0 -44
- package/i18n/ja-JP.properties +0 -44
- package/i18n/json/src/components/BoundingBoxHighlight/messages.json +0 -1
- package/i18n/json/src/components/Popups/messages.json +0 -1
- package/i18n/json/src/messages.json +0 -1
- package/i18n/ko-KR.properties +0 -44
- package/i18n/nb-NO.properties +0 -44
- package/i18n/nl-NL.properties +0 -44
- package/i18n/pl-PL.properties +0 -44
- package/i18n/pt-BR.properties +0 -44
- package/i18n/ru-RU.properties +0 -44
- package/i18n/sv-SE.properties +0 -44
- package/i18n/tr-TR.properties +0 -44
- package/i18n/zh-CN.properties +0 -44
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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.
|
|
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": "^
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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(
|
|
34
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
if (this.canCreate) {
|
|
163
|
+
const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
|
|
148
164
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
package/src/document/docUtil.ts
CHANGED
|
@@ -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);
|
package/src/popup/PopupLayer.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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');
|