box-annotations 4.0.0-beta.8 → 4.0.1-beta.1

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 (208) hide show
  1. package/README.md +7 -57
  2. package/dist/annotations.css +1 -1
  3. package/dist/annotations.js +1 -1
  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/en-US.js +6 -0
  17. package/i18n/en-US.properties +12 -0
  18. package/i18n/en-x-pseudo.js +16 -9
  19. package/i18n/en-x-pseudo.properties +23 -9
  20. package/i18n/es-419.js +6 -0
  21. package/i18n/es-419.properties +12 -0
  22. package/i18n/es-ES.js +6 -0
  23. package/i18n/es-ES.properties +12 -0
  24. package/i18n/fi-FI.js +6 -0
  25. package/i18n/fi-FI.properties +12 -0
  26. package/i18n/fr-CA.js +6 -0
  27. package/i18n/fr-CA.properties +12 -0
  28. package/i18n/fr-FR.js +6 -0
  29. package/i18n/fr-FR.properties +12 -0
  30. package/i18n/hi-IN.js +6 -0
  31. package/i18n/hi-IN.properties +12 -0
  32. package/i18n/it-IT.js +6 -0
  33. package/i18n/it-IT.properties +12 -0
  34. package/i18n/ja-JP.js +6 -0
  35. package/i18n/ja-JP.properties +12 -0
  36. package/i18n/json/src/components/Popups/messages.json +1 -1
  37. package/i18n/ko-KR.js +6 -0
  38. package/i18n/ko-KR.properties +12 -0
  39. package/i18n/nb-NO.js +6 -0
  40. package/i18n/nb-NO.properties +12 -0
  41. package/i18n/nl-NL.js +6 -0
  42. package/i18n/nl-NL.properties +12 -0
  43. package/i18n/pl-PL.js +6 -0
  44. package/i18n/pl-PL.properties +12 -0
  45. package/i18n/pt-BR.js +6 -0
  46. package/i18n/pt-BR.properties +12 -0
  47. package/i18n/ru-RU.js +6 -0
  48. package/i18n/ru-RU.properties +12 -0
  49. package/i18n/sv-SE.js +6 -0
  50. package/i18n/sv-SE.properties +12 -0
  51. package/i18n/tr-TR.js +6 -0
  52. package/i18n/tr-TR.properties +12 -0
  53. package/i18n/zh-CN.js +6 -0
  54. package/i18n/zh-CN.properties +12 -0
  55. package/i18n/zh-TW.js +6 -0
  56. package/i18n/zh-TW.properties +12 -0
  57. package/package.json +29 -29
  58. package/src/@types/events.ts +8 -2
  59. package/src/@types/global.ts +1 -0
  60. package/src/@types/model.ts +20 -7
  61. package/src/BoxAnnotations.ts +16 -2
  62. package/src/common/BaseAnnotator.scss +0 -1
  63. package/src/common/BaseAnnotator.ts +125 -19
  64. package/src/common/BaseManager.ts +67 -4
  65. package/src/common/DeselectListener.tsx +21 -0
  66. package/src/common/DeselectManager.tsx +30 -0
  67. package/src/common/useIsListInteractive.ts +10 -0
  68. package/src/common/useMountId.ts +14 -0
  69. package/src/common/useOutsideEvent.ts +9 -4
  70. package/src/common/usePrevious.ts +13 -0
  71. package/src/components/ItemList/ItemList.scss +7 -2
  72. package/src/components/ItemList/ItemList.tsx +45 -26
  73. package/src/components/ItemList/ItemRow.scss +19 -7
  74. package/src/components/ItemList/ItemRow.tsx +33 -12
  75. package/src/components/PointerCapture/PointerCapture.tsx +116 -0
  76. package/src/components/PointerCapture/index.ts +2 -0
  77. package/src/components/Popups/Popper.ts +1 -0
  78. package/src/components/Popups/PopupArrow.scss +91 -0
  79. package/src/components/Popups/PopupBase.scss +12 -81
  80. package/src/components/Popups/PopupBase.tsx +0 -1
  81. package/src/components/Popups/PopupCursor.tsx +1 -1
  82. package/src/components/Popups/PopupDrawingToolbar.scss +76 -0
  83. package/src/components/Popups/PopupDrawingToolbar.tsx +109 -0
  84. package/src/components/Popups/PopupHighlight.scss +39 -0
  85. package/src/components/Popups/PopupHighlight.tsx +97 -0
  86. package/src/components/Popups/PopupHighlightError.scss +23 -0
  87. package/src/components/Popups/PopupHighlightError.tsx +74 -0
  88. package/src/components/Popups/PopupList.scss +1 -4
  89. package/src/components/Popups/PopupList.tsx +2 -2
  90. package/src/components/Popups/PopupReply.tsx +51 -6
  91. package/src/components/Popups/messages.ts +30 -0
  92. package/src/components/ReplyField/ReplyField.scss +6 -1
  93. package/src/components/ReplyField/ReplyField.tsx +43 -42
  94. package/src/components/ReplyField/ReplyFieldContainer.tsx +2 -1
  95. package/src/components/ReplyForm/ReplyButton.tsx +38 -0
  96. package/src/components/ReplyForm/ReplyForm.scss +3 -0
  97. package/src/components/ReplyForm/ReplyForm.tsx +49 -12
  98. package/src/components/ReplyForm/ReplyFormContainer.tsx +6 -4
  99. package/src/constants.js +1 -8
  100. package/src/document/DocumentAnnotator.scss +15 -0
  101. package/src/document/DocumentAnnotator.ts +110 -53
  102. package/src/document/docUtil.ts +61 -0
  103. package/src/drawing/DecoratedDrawingPath.scss +9 -0
  104. package/src/drawing/DecoratedDrawingPath.tsx +43 -0
  105. package/src/drawing/DrawingAnnotations.scss +39 -0
  106. package/src/drawing/DrawingAnnotations.tsx +157 -0
  107. package/src/drawing/DrawingAnnotationsContainer.tsx +66 -0
  108. package/src/drawing/DrawingCreator.scss +9 -0
  109. package/src/drawing/DrawingCreator.tsx +183 -0
  110. package/src/drawing/DrawingCursor.ts +23 -0
  111. package/src/drawing/DrawingList.tsx +60 -0
  112. package/src/drawing/DrawingManager.tsx +15 -0
  113. package/src/drawing/DrawingPath.tsx +17 -0
  114. package/src/drawing/DrawingPathGroup.tsx +52 -0
  115. package/src/drawing/DrawingSVG.tsx +34 -0
  116. package/src/drawing/DrawingSVGGroup.tsx +23 -0
  117. package/src/drawing/DrawingTarget.scss +11 -0
  118. package/src/drawing/DrawingTarget.tsx +100 -0
  119. package/src/drawing/SVGFilterContext.js +5 -0
  120. package/src/drawing/actions.ts +48 -0
  121. package/src/drawing/drawingUtil.ts +106 -0
  122. package/src/drawing/index.ts +2 -0
  123. package/src/highlight/HighlightAnnotations.scss +12 -0
  124. package/src/highlight/HighlightAnnotations.tsx +144 -0
  125. package/src/highlight/HighlightCanvas.scss +9 -0
  126. package/src/highlight/HighlightCanvas.tsx +109 -0
  127. package/src/highlight/HighlightContainer.tsx +64 -0
  128. package/src/highlight/HighlightCreator.scss +12 -0
  129. package/src/highlight/HighlightCreatorManager.ts +79 -0
  130. package/src/highlight/HighlightList.scss +13 -0
  131. package/src/highlight/HighlightList.tsx +92 -0
  132. package/src/highlight/HighlightListener.ts +37 -0
  133. package/src/highlight/HighlightManager.tsx +15 -0
  134. package/src/highlight/HighlightSvg.scss +7 -0
  135. package/src/highlight/HighlightSvg.tsx +18 -0
  136. package/src/highlight/HighlightTarget.scss +5 -0
  137. package/src/highlight/HighlightTarget.tsx +106 -0
  138. package/src/highlight/actions.ts +36 -0
  139. package/src/highlight/highlightUtil.ts +143 -0
  140. package/src/highlight/index.ts +4 -0
  141. package/src/image/ImageAnnotator.scss +20 -0
  142. package/src/image/ImageAnnotator.ts +137 -0
  143. package/src/polyfill.ts +19 -0
  144. package/src/popup/PopupContainer.tsx +50 -0
  145. package/src/popup/PopupLayer.scss +3 -0
  146. package/src/popup/PopupLayer.tsx +103 -0
  147. package/src/popup/PopupManager.tsx +15 -0
  148. package/src/region/RegionAnnotation.scss +4 -0
  149. package/src/region/RegionAnnotation.tsx +15 -16
  150. package/src/region/RegionAnnotations.scss +1 -15
  151. package/src/region/RegionAnnotations.tsx +14 -115
  152. package/src/region/RegionAnnotationsContainer.tsx +24 -0
  153. package/src/region/RegionCreation.scss +17 -0
  154. package/src/region/RegionCreation.tsx +79 -0
  155. package/src/region/RegionCreationContainer.tsx +43 -0
  156. package/src/region/RegionCreationManager.tsx +15 -0
  157. package/src/region/RegionCreator.scss +9 -2
  158. package/src/region/RegionCreator.tsx +50 -71
  159. package/src/region/RegionList.tsx +8 -12
  160. package/src/region/RegionManager.tsx +7 -39
  161. package/src/region/RegionRect.scss +41 -9
  162. package/src/region/RegionRect.tsx +9 -3
  163. package/src/region/index.ts +1 -0
  164. package/src/region/regionUtil.ts +1 -8
  165. package/src/region/transformUtil.ts +28 -0
  166. package/src/store/annotations/actions.ts +1 -0
  167. package/src/store/annotations/reducer.ts +9 -2
  168. package/src/store/annotations/selectors.ts +1 -0
  169. package/src/store/annotations/types.ts +1 -0
  170. package/src/store/common/actions.ts +1 -0
  171. package/src/store/common/reducer.ts +16 -10
  172. package/src/store/common/selectors.ts +1 -0
  173. package/src/store/common/types.ts +3 -0
  174. package/src/store/createRootReducer.ts +4 -0
  175. package/src/store/creator/actions.ts +2 -0
  176. package/src/store/creator/reducer.ts +22 -3
  177. package/src/store/creator/selectors.ts +9 -1
  178. package/src/store/creator/types.ts +18 -3
  179. package/src/store/drawing/actions.ts +10 -0
  180. package/src/store/drawing/index.ts +4 -0
  181. package/src/store/drawing/reducer.ts +50 -0
  182. package/src/store/drawing/selectors.ts +9 -0
  183. package/src/store/drawing/types.ts +7 -0
  184. package/src/store/eventing/active.ts +4 -2
  185. package/src/store/eventing/fetch.ts +8 -0
  186. package/src/store/eventing/init.ts +14 -0
  187. package/src/store/eventing/middleware.ts +21 -3
  188. package/src/store/eventing/mode.ts +7 -0
  189. package/src/store/eventing/staged.ts +56 -0
  190. package/src/store/eventing/status.ts +19 -0
  191. package/src/store/highlight/actions.ts +106 -0
  192. package/src/store/highlight/index.ts +4 -0
  193. package/src/store/highlight/reducer.ts +37 -0
  194. package/src/store/highlight/selectors.ts +12 -0
  195. package/src/store/highlight/types.ts +14 -0
  196. package/src/store/index.ts +2 -0
  197. package/src/store/options/actions.ts +2 -0
  198. package/src/store/options/reducer.ts +17 -1
  199. package/src/store/options/selectors.ts +8 -0
  200. package/src/store/options/types.ts +5 -0
  201. package/src/store/types.ts +5 -1
  202. package/src/store/users/actions.ts +3 -2
  203. package/src/utils/resin.ts +22 -0
  204. package/src/utils/rotate.ts +131 -0
  205. package/src/utils/scroll.ts +45 -0
  206. package/src/utils/util.ts +3 -0
  207. package/src/region/RegionContainer.tsx +0 -49
  208. package/src/utils/polyfill.js +0 -161
@@ -1,18 +1,26 @@
1
+ import './polyfill';
1
2
  import getProp from 'lodash/get';
3
+ import BaseAnnotator from './common/BaseAnnotator';
4
+ import ImageAnnotator from './image/ImageAnnotator';
2
5
  import DocumentAnnotator from './document/DocumentAnnotator';
3
6
  import { IntlOptions, Permissions, PERMISSIONS, Type } from './@types';
4
7
 
5
8
  type Annotator = {
6
- CONSTRUCTOR: typeof DocumentAnnotator;
9
+ CONSTRUCTOR: typeof BaseAnnotator;
7
10
  NAME: string;
8
11
  TYPES: string[];
9
12
  VIEWERS: string[];
10
13
  };
11
14
 
12
15
  type AnnotationsOptions = {
16
+ features: Features;
13
17
  intl: IntlOptions;
14
18
  };
15
19
 
20
+ export type Features = {
21
+ [key: string]: boolean;
22
+ };
23
+
16
24
  type PreviewOptions = {
17
25
  file?: {
18
26
  permissions?: Permissions;
@@ -43,8 +51,14 @@ const ANNOTATORS: Annotator[] = [
43
51
  {
44
52
  CONSTRUCTOR: DocumentAnnotator,
45
53
  NAME: 'Document',
54
+ TYPES: [Type.highlight, Type.region],
55
+ VIEWERS: ['AutoCAD', 'Document', 'Presentation'],
56
+ },
57
+ {
58
+ CONSTRUCTOR: ImageAnnotator,
59
+ NAME: 'Image',
46
60
  TYPES: [Type.region],
47
- VIEWERS: ['Document', 'Presentation'],
61
+ VIEWERS: ['Image'],
48
62
  },
49
63
  ];
50
64
 
@@ -12,7 +12,6 @@
12
12
  src: local('Lato Bold'), local('Lato-Bold'), url('https://cdn01.boxcdn.net/fonts/1.0.2/lato/Lato-Bold.woff2') format('woff2'), url('https://cdn01.boxcdn.net/fonts/1.0.2/lato/Lato-Bold.woff') format('woff');
13
13
  }
14
14
 
15
- @import '~box-ui-elements/es/styles/reset';
16
15
  @import '~box-ui-elements/es/styles/variables';
17
16
 
18
17
  .ba {
@@ -1,10 +1,13 @@
1
+ import getProp from 'lodash/get';
1
2
  import { IntlShape } from 'react-intl';
2
3
  import * as store from '../store';
3
4
  import API from '../api';
5
+ import DeselectManager from './DeselectManager';
4
6
  import EventEmitter from './EventEmitter';
5
7
  import i18n from '../utils/i18n';
6
8
  import messages from '../messages';
7
9
  import { Event, IntlOptions, LegacyEvent, Permissions } from '../@types';
10
+ import { Features } from '../BoxAnnotations';
8
11
  import './BaseAnnotator.scss';
9
12
 
10
13
  export type Container = string | HTMLElement;
@@ -14,12 +17,15 @@ export type FileOptions = {
14
17
  annotations?: {
15
18
  activeId?: string;
16
19
  };
20
+ currentFileVersionId?: string;
21
+ fileVersionId?: string;
17
22
  };
18
23
  };
19
24
 
20
25
  export type Options = {
21
26
  apiHost: string;
22
27
  container: Container;
28
+ features?: Features;
23
29
  file: {
24
30
  id: string;
25
31
  file_version: {
@@ -29,37 +35,69 @@ export type Options = {
29
35
  };
30
36
  fileOptions?: FileOptions;
31
37
  hasTouch?: boolean;
38
+ initialColor?: string;
39
+ initialMode?: store.Mode;
32
40
  intl: IntlOptions;
33
41
  locale?: string;
34
42
  token: string;
35
43
  };
36
44
 
45
+ export const CSS_CONTAINER_CLASS = 'ba';
46
+ export const CSS_LOADED_CLASS = 'ba-annotations-loaded';
47
+ export const ANNOTATION_CLASSES: { [M in store.Mode]?: string } = {
48
+ [store.Mode.HIGHLIGHT]: 'ba-is-create--highlight',
49
+ [store.Mode.REGION]: 'ba-is-create--region',
50
+ };
51
+
37
52
  export default class BaseAnnotator extends EventEmitter {
53
+ annotatedEl?: HTMLElement | null;
54
+
55
+ containerEl?: HTMLElement | null;
56
+
38
57
  container: Container;
39
58
 
40
- intl: IntlShape;
59
+ deselectManager: DeselectManager | null = null;
41
60
 
42
- rootEl?: HTMLElement | null;
61
+ features: Features;
43
62
 
44
- scale = 1;
63
+ intl: IntlShape;
45
64
 
46
65
  store: store.AppStore;
47
66
 
48
- constructor({ apiHost, container, file, fileOptions, intl, token }: Options) {
67
+ constructor({
68
+ apiHost,
69
+ container,
70
+ features = {},
71
+ file,
72
+ fileOptions,
73
+ initialColor,
74
+ initialMode,
75
+ intl,
76
+ token,
77
+ }: Options) {
49
78
  super();
50
79
 
80
+ const fileOptionsValue = fileOptions?.[file.id];
81
+ const currentFileVersionId = fileOptionsValue?.currentFileVersionId;
82
+ const fileOptionsVersionId = fileOptionsValue?.fileVersionId;
83
+ const fileVersionId = file.file_version.id; // This is always the currently-previewed version id, not current version id
84
+
51
85
  const initialState = {
52
86
  annotations: {
53
- activeId: fileOptions?.[file.id]?.annotations?.activeId ?? null,
87
+ activeId: fileOptionsValue?.annotations?.activeId ?? null,
54
88
  },
89
+ common: { color: initialColor, mode: initialMode },
55
90
  options: {
91
+ features,
56
92
  fileId: file.id,
57
- fileVersionId: file.file_version.id,
93
+ fileVersionId: fileOptionsVersionId ?? fileVersionId,
94
+ isCurrentFileVersion: !fileOptionsVersionId || fileOptionsVersionId === currentFileVersionId,
58
95
  permissions: file.permissions,
59
96
  },
60
97
  };
61
98
 
62
99
  this.container = container;
100
+ this.features = features;
63
101
  this.intl = i18n.createIntlProvider(intl);
64
102
  this.store = store.createStore(initialState, {
65
103
  api: new API({ apiHost, token }),
@@ -69,6 +107,7 @@ export default class BaseAnnotator extends EventEmitter {
69
107
  this.addListener(LegacyEvent.SCALE, this.handleScale);
70
108
  this.addListener(Event.ACTIVE_SET, this.handleSetActive);
71
109
  this.addListener(Event.ANNOTATION_REMOVE, this.handleRemove);
110
+ this.addListener(Event.COLOR_SET, this.handleSetColor);
72
111
  this.addListener(Event.VISIBLE_SET, this.handleSetVisible);
73
112
 
74
113
  // Load any required data at startup
@@ -76,26 +115,51 @@ export default class BaseAnnotator extends EventEmitter {
76
115
  }
77
116
 
78
117
  public destroy(): void {
79
- if (this.rootEl) {
80
- this.rootEl.classList.remove('ba');
118
+ if (this.containerEl) {
119
+ this.containerEl.classList.remove(CSS_CONTAINER_CLASS);
81
120
  }
82
121
 
122
+ if (this.annotatedEl) {
123
+ this.annotatedEl.classList.remove(CSS_LOADED_CLASS);
124
+ }
125
+
126
+ if (this.deselectManager) {
127
+ this.deselectManager.destroy();
128
+ }
129
+
130
+ this.removeAnnotationClasses();
131
+
83
132
  this.removeListener(LegacyEvent.SCALE, this.handleScale);
84
133
  this.removeListener(Event.ACTIVE_SET, this.handleSetActive);
85
134
  this.removeListener(Event.ANNOTATION_REMOVE, this.handleRemove);
135
+ this.removeListener(Event.COLOR_SET, this.handleSetColor);
86
136
  this.removeListener(Event.VISIBLE_SET, this.handleSetVisible);
87
137
  }
88
138
 
89
- public init(scale: number): void {
90
- this.rootEl = this.getElement(this.container);
91
- this.scale = scale;
139
+ public init(scale = 1, rotation = 0): void {
140
+ this.containerEl = this.getElement(this.container);
141
+ this.annotatedEl = this.getAnnotatedElement();
92
142
 
93
- if (!this.rootEl) {
143
+ if (!this.annotatedEl || !this.containerEl) {
94
144
  this.emit(LegacyEvent.ERROR, this.intl.formatMessage(messages.annotationsLoadError));
95
145
  return;
96
146
  }
97
147
 
98
- this.rootEl.classList.add('ba');
148
+ // Add classes to the parent elements to support CSS scoping
149
+ this.annotatedEl.classList.add(CSS_LOADED_CLASS);
150
+ this.containerEl.classList.add(CSS_CONTAINER_CLASS);
151
+
152
+ // Defer to the child class to render annotations
153
+ this.render();
154
+
155
+ // Update the store now that annotations have been rendered
156
+ this.store.dispatch(store.setRotationAction(rotation));
157
+ this.store.dispatch(store.setScaleAction(scale));
158
+ this.store.dispatch(store.setIsInitialized());
159
+ }
160
+
161
+ public isFeatureEnabled(feature = ''): boolean {
162
+ return getProp(this.features, feature, false);
99
163
  }
100
164
 
101
165
  public removeAnnotation = (annotationId: string): void => {
@@ -111,14 +175,19 @@ export default class BaseAnnotator extends EventEmitter {
111
175
  this.store.dispatch(store.setActiveAnnotationIdAction(annotationId));
112
176
  }
113
177
 
178
+ public setColor(color: string): void {
179
+ this.store.dispatch(store.setColorAction(color));
180
+ }
181
+
114
182
  public setVisibility(visibility: boolean): void {
115
- if (!this.rootEl) {
183
+ if (!this.containerEl) {
116
184
  return;
117
185
  }
186
+
118
187
  if (visibility) {
119
- this.rootEl.classList.remove('is-hidden');
188
+ this.containerEl.classList.remove('is-hidden');
120
189
  } else {
121
- this.rootEl.classList.add('is-hidden');
190
+ this.containerEl.classList.add('is-hidden');
122
191
  }
123
192
  }
124
193
 
@@ -126,6 +195,10 @@ export default class BaseAnnotator extends EventEmitter {
126
195
  this.store.dispatch(store.toggleAnnotationModeAction(mode));
127
196
  }
128
197
 
198
+ protected getAnnotatedElement(): HTMLElement | null | undefined {
199
+ return undefined; // Must be implemented in child class
200
+ }
201
+
129
202
  protected getElement(selector: HTMLElement | string): HTMLElement | null {
130
203
  return typeof selector === 'string' ? document.querySelector(selector) : selector;
131
204
  }
@@ -134,8 +207,8 @@ export default class BaseAnnotator extends EventEmitter {
134
207
  this.removeAnnotation(annotationId);
135
208
  };
136
209
 
137
- protected handleScale = ({ scale }: { scale: number }): void => {
138
- this.init(scale);
210
+ protected handleScale = ({ rotationAngle, scale }: { rotationAngle: number; scale: number }): void => {
211
+ this.init(scale, rotationAngle);
139
212
  };
140
213
 
141
214
  protected handleSetActive = (annotationId: string | null): void => {
@@ -146,9 +219,42 @@ export default class BaseAnnotator extends EventEmitter {
146
219
  this.setVisibility(visibility);
147
220
  };
148
221
 
222
+ protected handleSetColor = (color: string): void => {
223
+ this.setColor(color);
224
+ };
225
+
149
226
  protected hydrate(): void {
150
227
  // Redux dispatch method signature doesn't seem to like async actions
151
228
  this.store.dispatch<any>(store.fetchAnnotationsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
152
- this.store.dispatch<any>(store.fetchCollaboratorsAction()); // eslint-disable-line @typescript-eslint/no-explicit-any
229
+ }
230
+
231
+ protected removeAnnotationClasses = (): void => {
232
+ if (!this.annotatedEl) {
233
+ return;
234
+ }
235
+
236
+ const annotatedElement = this.annotatedEl;
237
+ Object.values(ANNOTATION_CLASSES).forEach(className => {
238
+ if (className) {
239
+ annotatedElement.classList.remove(className);
240
+ }
241
+ });
242
+ };
243
+
244
+ protected render(): void {
245
+ // Must be implemented in child class
246
+ }
247
+
248
+ public postRender(): void {
249
+ if (!this.annotatedEl) {
250
+ return;
251
+ }
252
+
253
+ if (this.deselectManager) {
254
+ this.deselectManager.destroy();
255
+ }
256
+
257
+ this.deselectManager = new DeselectManager({ referenceEl: this.annotatedEl });
258
+ this.deselectManager.render({ store: this.store });
153
259
  }
154
260
  }
@@ -1,10 +1,14 @@
1
+ import * as ReactDOM from 'react-dom';
1
2
  import { IntlShape } from 'react-intl';
2
3
  import { Store } from 'redux';
4
+ import { applyResinTags } from '../utils/resin';
5
+
6
+ export type ResinTags = Record<string, unknown>;
3
7
 
4
8
  export type Options = {
5
- page: number;
6
- pageEl: HTMLElement;
9
+ location?: number;
7
10
  referenceEl: HTMLElement;
11
+ resinTags?: ResinTags;
8
12
  };
9
13
 
10
14
  export type Props = {
@@ -12,8 +16,67 @@ export type Props = {
12
16
  store: Store;
13
17
  };
14
18
 
15
- export default interface BaseManager {
19
+ export interface Manager {
16
20
  destroy(): void;
17
- exists(pageEl: HTMLElement): boolean;
21
+ exists(parentEl: HTMLElement): boolean;
18
22
  render(props: Props): void;
23
+ style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration;
24
+ }
25
+
26
+ export default class BaseManager implements Manager {
27
+ location: number;
28
+
29
+ reactEl: HTMLElement;
30
+
31
+ constructor({ location = 1, referenceEl, resinTags = {} }: Options) {
32
+ this.location = location;
33
+ this.reactEl = this.insert(referenceEl, {
34
+ ...resinTags,
35
+ feature: 'annotations',
36
+ });
37
+
38
+ this.decorate();
39
+ }
40
+
41
+ decorate(): void {
42
+ throw new Error('Method not implemented.');
43
+ }
44
+
45
+ destroy(): void {
46
+ ReactDOM.unmountComponentAtNode(this.reactEl);
47
+
48
+ const parentEl = this.reactEl.parentNode;
49
+ if (parentEl) {
50
+ parentEl.removeChild(this.reactEl);
51
+ }
52
+ }
53
+
54
+ exists(parentEl: HTMLElement): boolean {
55
+ return parentEl.contains(this.reactEl);
56
+ }
57
+
58
+ insert(referenceEl: HTMLElement, resinTags: ResinTags = {}): HTMLElement {
59
+ // Find the nearest applicable reference and document elements
60
+ const documentEl = referenceEl.ownerDocument || document;
61
+ const parentEl = referenceEl.parentNode || documentEl;
62
+
63
+ // Construct a layer element where we can inject a root React component
64
+ const rootLayerEl = documentEl.createElement('div');
65
+ rootLayerEl.classList.add('ba-Layer');
66
+
67
+ // Apply any resin tags
68
+ applyResinTags(rootLayerEl, resinTags);
69
+
70
+ // Insert the new layer element immediately after the reference element
71
+ return parentEl.insertBefore(rootLayerEl, referenceEl.nextSibling);
72
+ }
73
+
74
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
75
+ render(props: Props): void {
76
+ throw new Error('Method not implemented.');
77
+ }
78
+
79
+ style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration {
80
+ return Object.assign(this.reactEl.style, styles);
81
+ }
19
82
  }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { useDispatch } from 'react-redux';
3
+ import { setActiveAnnotationIdAction } from '../store/annotations';
4
+
5
+ export default function DeselectListener(): null {
6
+ const dispatch = useDispatch();
7
+
8
+ React.useEffect(() => {
9
+ const handleMouseDown = (): void => {
10
+ dispatch(setActiveAnnotationIdAction(null));
11
+ };
12
+
13
+ document.addEventListener('mousedown', handleMouseDown);
14
+
15
+ return () => {
16
+ document.removeEventListener('mousedown', handleMouseDown);
17
+ };
18
+ });
19
+
20
+ return null;
21
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ import { Provider } from 'react-redux';
4
+ import { Store } from 'redux';
5
+ import BaseManager from './BaseManager';
6
+ import DeselectListener from './DeselectListener';
7
+
8
+ export type Options = {
9
+ referenceEl: HTMLElement;
10
+ };
11
+
12
+ export type Props = {
13
+ store: Store;
14
+ };
15
+
16
+ export default class DeselectManager extends BaseManager {
17
+ decorate(): void {
18
+ this.reactEl.classList.add('ba-Layer--deselect');
19
+ this.reactEl.dataset.testid = 'ba-Layer--deselect';
20
+ }
21
+
22
+ render({ store }: Props): void {
23
+ ReactDOM.render(
24
+ <Provider store={store}>
25
+ <DeselectListener />
26
+ </Provider>,
27
+ this.reactEl,
28
+ );
29
+ }
30
+ }
@@ -0,0 +1,10 @@
1
+ import * as ReactRedux from 'react-redux';
2
+ import { getCreatorStatus, getIsSelecting, CreatorStatus } from '../store';
3
+
4
+ // Returns whether rendered annotations should be interactive
5
+ export default function useIsListInteractive(): boolean {
6
+ const status = ReactRedux.useSelector(getCreatorStatus);
7
+ const isSelecting = ReactRedux.useSelector(getIsSelecting);
8
+
9
+ return status === CreatorStatus.init && !isSelecting;
10
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import * as uuid from 'uuid';
3
+
4
+ // Returns a generated uuid, then calls the provided callback via an effect
5
+ export default function useMountId(callback: (uuid: string) => void): string {
6
+ const uuidRef = React.useRef<string>(uuid.v4());
7
+
8
+ // The callback is only invoked after the parent component has rendered
9
+ React.useEffect(() => {
10
+ callback(uuidRef.current);
11
+ }, [callback]);
12
+
13
+ return uuidRef.current;
14
+ }
@@ -1,13 +1,18 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  // Utilizes the useEffect hook to handle when an event is fired on the document, but outside of the specified element
4
- export default function useOutsideEvent(type: string, ref: React.RefObject<Element>, callback: () => void): void {
4
+ export default function useOutsideEvent(
5
+ type: string,
6
+ ref?: React.RefObject<Element> | Element | null,
7
+ callback?: EventListener,
8
+ ): void {
5
9
  React.useEffect(() => {
6
- function handleEvent(event: Event): void {
7
- const containsEventTarget = !!ref?.current?.contains(event.target as Node);
10
+ function handleEvent<T extends Event>(event: T): void {
11
+ const element = ref instanceof Element ? ref : ref?.current;
12
+ const containsEventTarget = !!element?.contains(event.target as Node);
8
13
 
9
14
  if (callback && !containsEventTarget) {
10
- callback();
15
+ callback(event);
11
16
  }
12
17
  }
13
18
 
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ // Returns the last provided value, then caches the new value via an effect
4
+ export default function usePrevious<T>(value: T): T | undefined {
5
+ const ref = React.useRef<T>();
6
+
7
+ // The useEffect callback is only invoked after the parent component has rendered
8
+ React.useEffect(() => {
9
+ ref.current = value;
10
+ }, [value]);
11
+
12
+ return ref.current;
13
+ }
@@ -1,5 +1,10 @@
1
1
  .ba {
2
- .ba-ItemList-row {
3
- padding: 5px 30px 5px 15px;
2
+ .ba-ItemList {
3
+ max-height: 300px;
4
+ margin: 0;
5
+ padding: 0;
6
+ overflow-y: auto;
7
+ text-align: left;
8
+ list-style: none;
4
9
  }
5
10
  }
@@ -1,11 +1,13 @@
1
- import React, { SyntheticEvent } from 'react';
1
+ import React from 'react';
2
2
  import noop from 'lodash/noop';
3
- import ItemRow from './ItemRow';
3
+ import scrollIntoView from 'scroll-into-view-if-needed';
4
+ import ItemRow, { ItemRowRef } from './ItemRow';
4
5
  import './ItemList.scss';
5
6
 
6
7
  export type Props<T extends { id: string }> = {
7
8
  activeItemIndex?: number;
8
- itemRowAs?: JSX.Element;
9
+ autoScroll?: boolean;
10
+ itemRowAs?: React.ElementType;
9
11
  items: T[];
10
12
  onActivate?: (index: number) => void;
11
13
  onSelect: (index: number, event: React.SyntheticEvent) => void;
@@ -13,32 +15,49 @@ export type Props<T extends { id: string }> = {
13
15
 
14
16
  const ItemList = <T extends { id: string }>({
15
17
  activeItemIndex = 0,
16
- itemRowAs = <ItemRow />,
18
+ autoScroll = false,
19
+ itemRowAs: ItemRowComponent = ItemRow,
17
20
  items,
18
21
  onActivate = noop,
19
22
  onSelect,
20
23
  ...rest
21
- }: Props<T>): JSX.Element => (
22
- <ul data-testid="ba-ItemList" role="listbox" {...rest}>
23
- {items.map((item, index) =>
24
- React.cloneElement(itemRowAs, {
25
- ...item,
26
- key: item.id,
27
- className: 'ba-ItemList-row',
28
- isActive: index === activeItemIndex,
29
- onClick: (event: SyntheticEvent) => {
30
- onSelect(index, event);
31
- },
32
- /* preventDefault on mousedown so blur doesn't happen before click */
33
- onMouseDown: (event: SyntheticEvent) => {
34
- event.preventDefault();
35
- },
36
- onMouseEnter: () => {
37
- onActivate(index);
38
- },
39
- }),
40
- )}
41
- </ul>
42
- );
24
+ }: Props<T>): JSX.Element => {
25
+ const activeRef = React.useCallback(
26
+ (activeEl: ItemRowRef) => {
27
+ if (!autoScroll || !activeEl) {
28
+ return;
29
+ }
30
+
31
+ scrollIntoView(activeEl, {
32
+ scrollMode: 'if-needed',
33
+ block: 'nearest',
34
+ });
35
+ },
36
+ [autoScroll],
37
+ );
38
+
39
+ return (
40
+ <ul className="ba-ItemList" data-testid="ba-ItemList" role="listbox" {...rest}>
41
+ {items.map((item, index) => (
42
+ <ItemRowComponent
43
+ key={item.id}
44
+ ref={index === activeItemIndex ? activeRef : null}
45
+ className="ba-ItemList-row"
46
+ isActive={index === activeItemIndex}
47
+ item={item}
48
+ onClick={(event: React.SyntheticEvent) => {
49
+ onSelect(index, event);
50
+ }}
51
+ onMouseDown={(event: React.SyntheticEvent) => {
52
+ event.preventDefault(); // preventDefault on mousedown so blur doesn't happen before click
53
+ }}
54
+ onMouseEnter={() => {
55
+ onActivate(index);
56
+ }}
57
+ />
58
+ ))}
59
+ </ul>
60
+ );
61
+ };
43
62
 
44
63
  export default ItemList;
@@ -1,11 +1,23 @@
1
1
  @import '~box-ui-elements/es/styles/variables';
2
2
 
3
- .ba-ItemRow-name {
4
- line-height: 15px;
5
- }
3
+ // increase specificity to override .bp styles
4
+ .ba {
5
+ .ba-ItemRow {
6
+ padding: 5px 30px 5px 15px;
7
+
8
+ &.is-active {
9
+ background-color: fade-out($bdl-gray, .95);
10
+ cursor: pointer;
11
+ }
12
+ }
13
+
14
+ .ba-ItemRow-name {
15
+ line-height: 15px;
16
+ }
6
17
 
7
- .ba-ItemRow-email {
8
- color: $bdl-gray-62;
9
- font-size: 11px;
10
- line-height: 15px;
18
+ .ba-ItemRow-email {
19
+ color: $bdl-gray-62;
20
+ font-size: 11px;
21
+ line-height: 15px;
22
+ }
11
23
  }