box-annotations 5.2.1-beta.12 → 5.2.1-beta.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "box-annotations",
3
- "version": "5.2.1-beta.12",
3
+ "version": "5.2.1-beta.14",
4
4
  "description": "Box Annotations",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -28,13 +28,13 @@
28
28
  "@babel/preset-react": "^7.25.9",
29
29
  "@babel/preset-typescript": "^7.25.9",
30
30
  "@box/frontend": "^10.0.0",
31
- "@box/blueprint-web": "^14.27.0",
32
- "@box/blueprint-web-assets": "^4.116.6",
33
- "@box/collaboration-popover": "^1.61.30",
31
+ "@box/blueprint-web": "^14.31.0",
32
+ "@box/blueprint-web-assets": "^4.118.3",
33
+ "@box/collaboration-popover": "^1.62.9",
34
34
  "@box/languages": "^1.1.0",
35
- "@box/readable-time": "^1.40.30",
36
- "@box/threaded-annotations": "^1.86.0",
37
- "@box/user-selector": "^1.75.30",
35
+ "@box/readable-time": "^1.41.9",
36
+ "@box/threaded-annotations": "^1.89.8",
37
+ "@box/user-selector": "^1.76.9",
38
38
  "@cfaester/enzyme-adapter-react-18": "^0.8.0",
39
39
  "@commitlint/cli": "^8.3.5",
40
40
  "@commitlint/config-conventional": "^8.2.0",
@@ -128,12 +128,12 @@
128
128
  "worker-farm": "^1.7.0"
129
129
  },
130
130
  "peerDependencies": {
131
- "@box/blueprint-web": "^14.27.0",
132
- "@box/blueprint-web-assets": "^4.116.6",
133
- "@box/collaboration-popover": "^1.61.30",
134
- "@box/readable-time": "^1.40.30",
135
- "@box/threaded-annotations": "^1.86.0",
136
- "@box/user-selector": "^1.75.30"
131
+ "@box/blueprint-web": "^14.31.0",
132
+ "@box/blueprint-web-assets": "^4.118.3",
133
+ "@box/collaboration-popover": "^1.62.9",
134
+ "@box/readable-time": "^1.41.9",
135
+ "@box/threaded-annotations": "^1.89.8",
136
+ "@box/user-selector": "^1.76.9"
137
137
  },
138
138
  "scripts": {
139
139
  "build": "yarn setup && yarn build:prod:dist",
@@ -86,6 +86,13 @@ export interface Rect extends Position {
86
86
  width: number;
87
87
  }
88
88
 
89
+ export interface ReplyPermissions {
90
+ can_delete?: boolean;
91
+ can_edit?: boolean;
92
+ can_reply?: boolean;
93
+ can_resolve?: boolean;
94
+ }
95
+
89
96
  export interface Reply {
90
97
  created_at: string;
91
98
  created_by: User;
@@ -95,6 +102,7 @@ export interface Reply {
95
102
  id: string;
96
103
  type: string;
97
104
  };
105
+ permissions?: ReplyPermissions;
98
106
  type: 'reply';
99
107
  }
100
108
 
@@ -16,6 +16,7 @@ type Annotator = {
16
16
  type AnnotationsOptions = {
17
17
  features: Features;
18
18
  intl: IntlOptions;
19
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
19
20
  };
20
21
 
21
22
  export type Features = {
@@ -82,35 +82,28 @@ export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({
82
82
  id: reply.id,
83
83
  message: deserializeMentionMarkup(reply.message),
84
84
  permissions: {
85
- canDelete: false,
85
+ canDelete: reply.permissions?.can_delete ?? false,
86
86
  canEdit: false,
87
- canReply: true,
88
- canResolve: false,
87
+ canReply: reply.permissions?.can_reply ?? false,
88
+ canResolve: reply.permissions?.can_resolve ?? false,
89
89
  },
90
90
  });
91
91
 
92
- /**
93
- * Converts an Annotation's description (root message) to a TextMessageType.
94
- * The root message gets resolve permission from the annotation's permissions.
95
- * Falls back to annotation-level fields when the description object is sparse
96
- * (the list endpoint returns description with only { message }).
97
- */
98
- const descriptionToTextMessage = (
99
- annotation: Annotation,
100
- reply: Reply,
101
- ): TextMessageTypeV2 => ({
92
+ // The root message shares the annotation's author and permissions; description
93
+ // comes back sparse ({ message } only) from the list endpoint.
94
+ const descriptionToTextMessage = (annotation: Annotation): TextMessageTypeV2 => ({
102
95
  author: {
103
- email: (reply.created_by ?? annotation.created_by)?.login ?? '',
104
- id: parseInt((reply.created_by ?? annotation.created_by)?.id ?? '0', 10),
105
- name: (reply.created_by ?? annotation.created_by)?.name ?? '',
96
+ email: annotation.created_by?.login ?? '',
97
+ id: parseInt(annotation.created_by?.id ?? '0', 10),
98
+ name: annotation.created_by?.name ?? '',
106
99
  },
107
- createdAt: new Date(reply.created_at ?? annotation.created_at).getTime(),
108
- id: reply.id ?? annotation.id,
109
- message: deserializeMentionMarkup(reply.message),
100
+ createdAt: new Date(annotation.created_at).getTime(),
101
+ id: annotation.id,
102
+ message: deserializeMentionMarkup(annotation.description?.message ?? ''),
110
103
  permissions: {
111
104
  canDelete: annotation.permissions?.can_delete ?? false,
112
105
  canEdit: annotation.permissions?.can_edit ?? false,
113
- canReply: true,
106
+ canReply: annotation.permissions?.can_reply ?? false,
114
107
  canResolve: annotation.permissions?.can_resolve ?? false,
115
108
  },
116
109
  });
@@ -123,7 +116,7 @@ export const annotationToMessages = (annotation: Annotation): TextMessageTypeV2[
123
116
  const messages: TextMessageTypeV2[] = [];
124
117
 
125
118
  if (annotation.description) {
126
- messages.push(descriptionToTextMessage(annotation, annotation.description));
119
+ messages.push(descriptionToTextMessage(annotation));
127
120
  }
128
121
 
129
122
  if (annotation.replies) {
@@ -0,0 +1,7 @@
1
+ import { createContext } from 'react';
2
+
3
+ export type AnnotationCallbacks = {
4
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
5
+ };
6
+
7
+ export default createContext<AnnotationCallbacks>({});
@@ -44,6 +44,7 @@ export type Options = {
44
44
  initialViewMode?: ViewMode;
45
45
  intl: IntlOptions;
46
46
  locale?: string;
47
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
47
48
  token: string;
48
49
  };
49
50
 
@@ -70,6 +71,8 @@ export default class BaseAnnotator extends EventEmitter {
70
71
 
71
72
  intl: IntlShape;
72
73
 
74
+ onCopyLink?: (params: { annotationId: string; fileVersionId: string }) => void;
75
+
73
76
  store: store.AppStore;
74
77
 
75
78
  constructor({
@@ -82,6 +85,7 @@ export default class BaseAnnotator extends EventEmitter {
82
85
  initialMode,
83
86
  initialViewMode = 'annotations',
84
87
  intl,
88
+ onCopyLink,
85
89
  token,
86
90
  }: Options) {
87
91
  super();
@@ -111,6 +115,7 @@ export default class BaseAnnotator extends EventEmitter {
111
115
  this.container = container;
112
116
  this.features = features;
113
117
  this.intl = i18n.createIntlProvider(intl);
118
+ this.onCopyLink = onCopyLink;
114
119
  this.store = store.createStore(initialState, {
115
120
  api: new API({ apiHost, token }),
116
121
  });
@@ -1,6 +1,7 @@
1
1
  import * as ReactDOM from 'react-dom/client';
2
2
  import { IntlShape } from 'react-intl';
3
3
  import { Store } from 'redux';
4
+ import { AnnotationCallbacks } from './AnnotationCallbacksContext';
4
5
  import { applyResinTags } from '../utils/resin';
5
6
  import { TARGET_TYPE } from '../constants';
6
7
 
@@ -14,6 +15,7 @@ export type Options = {
14
15
  };
15
16
 
16
17
  export type Props = {
18
+ callbacks?: AnnotationCallbacks;
17
19
  intl: IntlShape;
18
20
  store: Store;
19
21
  };
@@ -2,8 +2,10 @@ import * as React from 'react';
2
2
  import { IntlShape, RawIntlProvider } from 'react-intl';
3
3
  import { Provider as StoreProvider } from 'react-redux';
4
4
  import { Store } from 'redux';
5
+ import AnnotationCallbacksContext, { AnnotationCallbacks } from './AnnotationCallbacksContext';
5
6
 
6
7
  type WrapperProps = {
8
+ callbacks?: AnnotationCallbacks;
7
9
  intl: IntlShape;
8
10
  store: Store;
9
11
  };
@@ -11,11 +13,13 @@ type WrapperProps = {
11
13
  type WrapperReturn<P> = React.FC<P & WrapperProps>;
12
14
 
13
15
  export default function withProviders<P extends object>(WrappedComponent: React.ComponentType<P>): WrapperReturn<P> {
14
- return function RootProvider({ intl, store, ...rest }: P & WrapperProps): JSX.Element {
16
+ return function RootProvider({ callbacks, intl, store, ...rest }: P & WrapperProps): JSX.Element {
15
17
  return (
16
18
  <RawIntlProvider value={intl}>
17
19
  <StoreProvider store={store}>
18
- <WrappedComponent {...(rest as P)} />
20
+ <AnnotationCallbacksContext.Provider value={callbacks ?? {}}>
21
+ <WrappedComponent {...(rest as P)} />
22
+ </AnnotationCallbacksContext.Provider>
19
23
  </StoreProvider>
20
24
  </RawIntlProvider>
21
25
  );
@@ -17,6 +17,7 @@ import type { JSONContent } from '@tiptap/core';
17
17
  import FocusTrap from 'box-ui-elements/es/components/focus-trap/FocusTrap';
18
18
 
19
19
  import { annotationToMessages, collaboratorToUserContact } from '../../adapters/threadedAnnotationsAdapters';
20
+ import AnnotationCallbacksContext from '../../common/AnnotationCallbacksContext';
20
21
  import {
21
22
  createReplyAction,
22
23
  deleteAnnotationAction,
@@ -24,7 +25,7 @@ import {
24
25
  updateAnnotationAction,
25
26
  } from '../../store/annotations/actions';
26
27
  import { getAnnotation } from '../../store/annotations/selectors';
27
- import { getApiHost, getToken } from '../../store/options';
28
+ import { getApiHost, getFileVersionId, getToken } from '../../store/options';
28
29
  import { fetchCollaboratorsAction } from '../../store/users/actions';
29
30
 
30
31
  import type { AppState, AppThunkDispatch } from '../../store/types';
@@ -80,12 +81,21 @@ const fetchAvatarBlob = async (apiHost: string, token: string, userId: string):
80
81
  const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): JSX.Element | null => {
81
82
  const intl = useIntl();
82
83
  const dispatch = useDispatch<AppThunkDispatch>();
84
+ const { onCopyLink: consumerOnCopyLink } = React.useContext(AnnotationCallbacksContext);
83
85
  const popupRef = React.useRef<HTMLDivElement>(null);
84
86
  const popperRef = React.useRef<Instance>();
85
87
  const optionsRef = React.useRef<Partial<Options>>(getPopupOptions());
86
88
 
87
89
  const apiHost = useSelector(getApiHost);
90
+ const fileVersionId = useSelector(getFileVersionId);
88
91
  const token = useSelector(getToken);
92
+ const onCopyLink = React.useMemo(
93
+ () =>
94
+ consumerOnCopyLink && annotationId && fileVersionId
95
+ ? () => consumerOnCopyLink({ annotationId, fileVersionId })
96
+ : undefined,
97
+ [consumerOnCopyLink, annotationId, fileVersionId],
98
+ );
89
99
  const annotation = useSelector((state: AppState) =>
90
100
  annotationId ? getAnnotation(state, annotationId) : undefined,
91
101
  );
@@ -235,6 +245,16 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
235
245
  [annotationId, dispatch, onSubmit],
236
246
  );
237
247
 
248
+ const handleEdit = React.useCallback(
249
+ async (id: string, content: JSONContent | null): Promise<void> => {
250
+ if (!annotationId || id !== annotationId) return;
251
+ const doc = createDocumentNode(content);
252
+ const { text } = serializeMentionMarkup(doc);
253
+ await dispatch(updateAnnotationAction({ annotationId, payload: { message: text } }));
254
+ },
255
+ [annotationId, dispatch],
256
+ );
257
+
238
258
  const handleThreadDelete = React.useCallback(
239
259
  async (): Promise<void> => {
240
260
  if (annotationId) {
@@ -293,7 +313,9 @@ const PopupV2 = ({ annotationId, onSubmit, popupPortalEl, reference }: Props): J
293
313
  isResolved={isResolved}
294
314
  messages={threadMessages}
295
315
  onAvatarClick={noop}
316
+ onCopyLink={onCopyLink}
296
317
  onDelete={noop}
318
+ onEdit={handleEdit}
297
319
  onPost={handlePost}
298
320
  onResolve={handleResolve}
299
321
  onThreadDelete={handleThreadDelete}
@@ -118,7 +118,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
118
118
  );
119
119
  }
120
120
 
121
- managers.add(new HighlightManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
121
+ managers.add(new HighlightManager({ location: pageNumber, popupPortalEl: this.popupPortalEl, referenceEl: pageReferenceEl, resinTags }));
122
122
  managers.add(new RegionManager({ location: pageNumber, referenceEl: pageReferenceEl, resinTags }));
123
123
 
124
124
  const canvasLayerEl = pageEl.querySelector<HTMLElement>('.canvasWrapper');
@@ -224,6 +224,7 @@ export default class DocumentAnnotator extends BaseAnnotator {
224
224
  }
225
225
 
226
226
  manager.render({
227
+ callbacks: { onCopyLink: this.onCopyLink },
227
228
  intl: this.intl,
228
229
  store: this.store,
229
230
  });
@@ -1,4 +1,5 @@
1
1
  type Selection = {
2
+ containerEl: HTMLElement;
2
3
  containerRect: DOMRect;
3
4
  hasError?: boolean;
4
5
  location: number;
@@ -61,5 +62,5 @@ export function getSelection(): Selection | null {
61
62
  location = getPageNumber(selection.focusNode as Element) ?? endPage;
62
63
  }
63
64
 
64
- return { containerRect: containerEl.getBoundingClientRect(), hasError, location, range };
65
+ return { containerEl: containerEl as HTMLElement, containerRect: containerEl.getBoundingClientRect(), hasError, location, range };
65
66
  }
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import ReactDOM from 'react-dom';
2
3
  import HighlightCanvas from './HighlightCanvas';
3
4
  import HighlightList from './HighlightList';
4
5
  import HighlightSvg from './HighlightSvg';
@@ -20,6 +21,8 @@ type Props = {
20
21
  isPromoting: boolean;
21
22
  isSelecting: boolean;
22
23
  location: number;
24
+ popupPortalEl?: HTMLElement | null;
25
+ rotation: number;
23
26
  selection: SelectionItem | null;
24
27
  setActiveAnnotationId: (annotationId: string | null) => void;
25
28
  setIsPromoting: (isPromoting: boolean) => void;
@@ -38,6 +41,8 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
38
41
  isPending = false,
39
42
  isPromoting = false,
40
43
  isSelecting = false,
44
+ popupPortalEl,
45
+ rotation,
41
46
  selection,
42
47
  setActiveAnnotationId,
43
48
  setIsPromoting,
@@ -121,22 +126,30 @@ const HighlightAnnotations = (props: Props): JSX.Element => {
121
126
  )}
122
127
 
123
128
  {/* Layer 3a: Annotations promoter to promote selection to staged */}
124
- {!isCreating && selection && !selection.hasError && (
125
- <div className="ba-HighlightAnnotations-popup">
126
- <PopupHighlight
127
- onCancel={handleCancel}
128
- onSubmit={handlePromote}
129
- shape={getBoundingRect(selection.rects)}
130
- />
131
- </div>
132
- )}
129
+ {!isCreating && selection && !selection.hasError && (() => {
130
+ const popupShape = selection.popupRect || getBoundingRect(selection.rects);
131
+ const popup = (
132
+ <div className="ba-HighlightAnnotations-popup">
133
+ <PopupHighlight
134
+ onCancel={handleCancel}
135
+ onSubmit={handlePromote}
136
+ shape={popupShape}
137
+ />
138
+ </div>
139
+ );
140
+ return rotation && popupPortalEl ? ReactDOM.createPortal(popup, popupPortalEl) : popup;
141
+ })()}
133
142
 
134
143
  {/* Layer 3b: Highlight error popup */}
135
- {selection && selection.hasError && (
136
- <div className="ba-HighlightAnnotations-popup">
137
- <PopupHighlightError onCancel={handleCancel} shape={getBoundingRect(selection.rects)} />
138
- </div>
139
- )}
144
+ {selection && selection.hasError && (() => {
145
+ const popupShape = selection.popupRect || getBoundingRect(selection.rects);
146
+ const popup = (
147
+ <div className="ba-HighlightAnnotations-popup">
148
+ <PopupHighlightError onCancel={handleCancel} shape={popupShape} />
149
+ </div>
150
+ );
151
+ return rotation && popupPortalEl ? ReactDOM.createPortal(popup, popupPortalEl) : popup;
152
+ })()}
140
153
  </>
141
154
  );
142
155
  };
@@ -13,6 +13,7 @@ import {
13
13
  getCreatorStatus,
14
14
  getIsPromoting,
15
15
  getIsSelecting,
16
+ getRotation,
16
17
  getSelectionForLocation,
17
18
  isCreatorStagedHighlight,
18
19
  Mode,
@@ -33,6 +34,7 @@ export type Props = {
33
34
  isPending: boolean;
34
35
  isPromoting: boolean;
35
36
  isSelecting: boolean;
37
+ rotation: number;
36
38
  selection: SelectionItem | null;
37
39
  staged: CreatorItemHighlight | null;
38
40
  };
@@ -47,6 +49,7 @@ export const mapStateToProps = (state: AppState, { location }: { location: numbe
47
49
  isPending: getCreatorStatus(state) === CreatorStatus.pending,
48
50
  isPromoting: getIsPromoting(state),
49
51
  isSelecting: getIsSelecting(state),
52
+ rotation: getRotation(state),
50
53
  selection: getSelectionForLocation(state, location),
51
54
  staged: isCreatorStagedHighlight(staged) ? staged : null,
52
55
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AppStore,
3
3
  getIsSelecting,
4
+ getRotation,
4
5
  SelectionArg as Selection,
5
6
  setIsSelectingAction,
6
7
  setSelectionAction,
@@ -41,18 +42,18 @@ export default class HighlightCreatorManager implements Manager {
41
42
  this.store.dispatch(setSelectionAction(null));
42
43
 
43
44
  this.referenceEl.addEventListener('mousedown', this.handleMouseDown);
44
- this.referenceEl.addEventListener('mouseup', this.handleMouseUp);
45
+ document.addEventListener('mouseup', this.handleMouseUp);
45
46
  }
46
47
 
47
- style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration {
48
- return Object.assign(this.referenceEl.style, styles);
48
+ style(): CSSStyleDeclaration {
49
+ return this.referenceEl.style;
49
50
  }
50
51
 
51
52
  destroy(): void {
52
53
  clearTimeout(this.selectionChangeTimer);
53
54
 
54
55
  this.referenceEl.removeEventListener('mousedown', this.handleMouseDown);
55
- this.referenceEl.removeEventListener('mouseup', this.handleMouseUp);
56
+ document.removeEventListener('mouseup', this.handleMouseUp);
56
57
  }
57
58
 
58
59
  handleMouseDown = ({ buttons }: MouseEvent): void => {
@@ -72,7 +73,9 @@ export default class HighlightCreatorManager implements Manager {
72
73
  }
73
74
 
74
75
  this.selectionChangeTimer = window.setTimeout(() => {
75
- this.store.dispatch(setSelectionAction(this.getSelection()));
76
+ const selection = this.getSelection();
77
+ const rotation = getRotation(this.store.getState());
78
+ this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
76
79
  this.store.dispatch(setIsSelectingAction(false));
77
80
  }, this.selectionChangeDelay);
78
81
  };
@@ -1,5 +1,5 @@
1
1
  import debounce from 'lodash/debounce';
2
- import { AppStore, getIsSelecting, SelectionArg as Selection, setSelectionAction } from '../store';
2
+ import { AppStore, getIsSelecting, getRotation, SelectionArg as Selection, setSelectionAction } from '../store';
3
3
 
4
4
  export type Options = {
5
5
  getSelection: () => Selection | null;
@@ -30,7 +30,9 @@ export default class HighlightListener {
30
30
  return;
31
31
  }
32
32
 
33
- this.store.dispatch(setSelectionAction(this.getSelection()));
33
+ const selection = this.getSelection();
34
+ const rotation = getRotation(this.store.getState());
35
+ this.store.dispatch(setSelectionAction(selection ? { ...selection, rotation } : null));
34
36
  };
35
37
 
36
38
  debounceHandleSelectionChange = debounce(this.handleSelectionChange, SELECTION_CHANGE_DEBOUNCE);
@@ -1,9 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import * as ReactDOM from 'react-dom/client';
3
- import BaseManager, { Props } from '../common/BaseManager';
3
+ import BaseManager, { Options as BaseOptions, Props } from '../common/BaseManager';
4
4
  import HighlightContainer from './HighlightContainer';
5
5
 
6
+ export type Options = BaseOptions & {
7
+ popupPortalEl?: HTMLElement | null;
8
+ };
9
+
6
10
  export default class HighlightManager extends BaseManager {
11
+ popupPortalEl?: HTMLElement | null;
12
+
13
+ constructor({ popupPortalEl, ...options }: Options) {
14
+ super(options);
15
+ this.popupPortalEl = popupPortalEl;
16
+ }
17
+
7
18
  decorate(): void {
8
19
  this.reactEl.classList.add('ba-Layer--highlight');
9
20
  this.reactEl.dataset.testid = 'ba-Layer--highlight';
@@ -14,6 +25,6 @@ export default class HighlightManager extends BaseManager {
14
25
  this.root = ReactDOM.createRoot(this.reactEl);
15
26
  }
16
27
 
17
- this.root.render(<HighlightContainer location={this.location} {...props} />);
28
+ this.root.render(<HighlightContainer location={this.location} popupPortalEl={this.popupPortalEl} {...props} />);
18
29
  }
19
30
  }
@@ -124,6 +124,7 @@ export default class ImageAnnotator extends BaseAnnotator {
124
124
  });
125
125
 
126
126
  manager.render({
127
+ callbacks: { onCopyLink: this.onCopyLink },
127
128
  intl: this.intl,
128
129
  store: this.store,
129
130
  });
@@ -108,6 +108,7 @@ export default class MediaAnnotator extends BaseAnnotator {
108
108
  width: `${referenceEl.offsetWidth}px`,
109
109
  });
110
110
  manager.render({
111
+ callbacks: { onCopyLink: this.onCopyLink },
111
112
  intl: this.intl,
112
113
  store: this.store,
113
114
  });
@@ -20,16 +20,18 @@ export default class PopupManager extends BaseManager {
20
20
  this.reactEl.dataset.testid = 'ba-Layer--popup';
21
21
  }
22
22
 
23
- render(props: Props): void {
23
+ render({ callbacks, intl, store }: Props): void {
24
24
  if (!this.root) {
25
25
  this.root = ReactDOM.createRoot(this.reactEl);
26
26
  }
27
27
 
28
28
  this.root.render(
29
29
  <PopupContainer
30
+ callbacks={callbacks}
31
+ intl={intl}
30
32
  location={this.location}
31
- {...props}
32
33
  popupPortalEl={this.popupPortalEl}
34
+ store={store}
33
35
  targetType={this.targetType}
34
36
  />,
35
37
  );
@@ -1,13 +1,16 @@
1
1
  import { createAction } from '@reduxjs/toolkit';
2
- import { combineRects } from '../../highlight/highlightUtil';
2
+ import { combineRects, getBoundingRect } from '../../highlight/highlightUtil';
3
+ import { getElementLocalPosition } from '../../utils/rotate';
3
4
  import { SelectionItem } from './types';
4
5
  import { Shape } from '../../@types';
5
6
 
6
7
  export type SelectionArg = {
8
+ containerEl?: HTMLElement | null;
7
9
  containerRect: DOMRect;
8
10
  hasError?: boolean;
9
11
  location: number;
10
12
  range: Range;
13
+ rotation?: number;
11
14
  };
12
15
 
13
16
  type Payload = {
@@ -83,7 +86,7 @@ export const setSelectionAction = createAction(
83
86
  };
84
87
  }
85
88
 
86
- const { containerRect, hasError, location, range } = arg;
89
+ const { containerEl, containerRect, hasError, location, range, rotation } = arg;
87
90
 
88
91
  let rects = Array.from(range.getClientRects());
89
92
  // getClientRects on IE/Edge might return 0 rects
@@ -91,12 +94,38 @@ export const setSelectionAction = createAction(
91
94
  rects = getClientRects(range);
92
95
  }
93
96
 
97
+ const screenShapes = rects.map(getShape);
98
+
99
+ if (rotation && containerEl) {
100
+ // Convert screen rects to element-local coordinates
101
+ const localRects = screenShapes.map(shape => {
102
+ const [lx1, ly1] = getElementLocalPosition(shape.x, shape.y, containerEl, rotation);
103
+ const [lx2, ly2] = getElementLocalPosition(shape.x + shape.width, shape.y + shape.height, containerEl, rotation);
104
+ return {
105
+ x: Math.min(lx1, lx2),
106
+ y: Math.min(ly1, ly2),
107
+ width: Math.abs(lx2 - lx1),
108
+ height: Math.abs(ly2 - ly1),
109
+ };
110
+ });
111
+
112
+ return {
113
+ payload: {
114
+ containerRect: { x: 0, y: 0, width: containerEl.offsetWidth, height: containerEl.offsetHeight },
115
+ hasError,
116
+ location,
117
+ popupRect: getBoundingRect(screenShapes),
118
+ rects: combineRects(localRects),
119
+ },
120
+ };
121
+ }
122
+
94
123
  return {
95
124
  payload: {
96
125
  containerRect: getShape(containerRect),
97
126
  hasError,
98
127
  location,
99
- rects: combineRects(rects.map(getShape)),
128
+ rects: combineRects(screenShapes),
100
129
  },
101
130
  };
102
131
  },
@@ -10,5 +10,6 @@ export type SelectionItem = {
10
10
  containerRect: Shape;
11
11
  hasError?: boolean;
12
12
  location: number;
13
+ popupRect?: Shape;
13
14
  rects: Array<Shape>;
14
15
  };