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
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import IconHighlightTextAnnotation from 'box-ui-elements/es/icon/fill/AnnotationsHighlight16';
3
+ import noop from 'lodash/noop';
4
+ import { FormattedMessage } from 'react-intl';
5
+ import messages from './messages';
6
+ import PopupBase from './PopupBase';
7
+ import useOutsideEvent from '../../common/useOutsideEvent';
8
+ import { Options } from './Popper';
9
+ import { Shape } from '../../@types/model';
10
+ import './PopupHighlight.scss';
11
+
12
+ export type Props = {
13
+ onCancel?: () => void;
14
+ onSubmit?: () => void;
15
+ shape: Shape;
16
+ };
17
+
18
+ const options: Partial<Options> = {
19
+ modifiers: [
20
+ {
21
+ name: 'arrow',
22
+ options: {
23
+ element: '.ba-Popup-arrow',
24
+ },
25
+ },
26
+ {
27
+ name: 'eventListeners',
28
+ options: {
29
+ scroll: false,
30
+ },
31
+ },
32
+ {
33
+ name: 'offset',
34
+ options: {
35
+ offset: [0, 8],
36
+ },
37
+ },
38
+ {
39
+ name: 'preventOverflow',
40
+ options: {
41
+ padding: 5,
42
+ },
43
+ },
44
+ ],
45
+ placement: 'bottom',
46
+ };
47
+
48
+ export default function PopupHighlight({ onCancel = noop, onSubmit = noop, shape }: Props): JSX.Element {
49
+ const buttonRef = React.useRef<HTMLButtonElement>(null);
50
+ const { height, width, x, y } = shape;
51
+
52
+ const reference = {
53
+ getBoundingClientRect: () => ({
54
+ bottom: y + height,
55
+ height,
56
+ left: x,
57
+ right: x + width,
58
+ top: y,
59
+ width,
60
+ }),
61
+ };
62
+
63
+ const handleKeydownOutside = (event: Event): void => {
64
+ if (!(event instanceof KeyboardEvent) || event.key !== 'Enter' || event.ctrlKey || event.metaKey) {
65
+ return;
66
+ }
67
+
68
+ event.preventDefault(); // Consume any enter keydown event if the promoter is open
69
+ event.stopPropagation(); // Prevent the enter keydown event from adding a new line to the textarea
70
+
71
+ onSubmit();
72
+ };
73
+
74
+ useOutsideEvent('keydown', buttonRef, handleKeydownOutside);
75
+ useOutsideEvent('mousedown', buttonRef, onCancel);
76
+
77
+ return (
78
+ <PopupBase
79
+ aria-live="polite"
80
+ className="ba-PopupHighlight"
81
+ options={options}
82
+ reference={reference}
83
+ role="dialog"
84
+ >
85
+ <button
86
+ ref={buttonRef}
87
+ className="ba-PopupHighlight-button"
88
+ data-testid="ba-PopupHighlight-button"
89
+ onClick={onSubmit}
90
+ type="button"
91
+ >
92
+ <IconHighlightTextAnnotation className="ba-PopupHighlight-icon" />
93
+ <FormattedMessage {...messages.popupHighlightPromoter} />
94
+ </button>
95
+ </PopupBase>
96
+ );
97
+ }
@@ -0,0 +1,23 @@
1
+ @import '~box-ui-elements/es/styles/variables';
2
+ @import './PopupArrow';
3
+
4
+ .ba {
5
+ $popup-content-bg: $bdl-gray-50;
6
+
7
+ .ba-PopupHighlightError {
8
+ @include ba-PopupArrow(4px, $popup-content-bg, $popup-content-bg, $popup-content-bg, $popup-content-bg, $popup-content-bg);
9
+ @include common-typography;
10
+
11
+ .ba-Popup-content {
12
+ display: flex;
13
+ flex-direction: row;
14
+ align-items: center;
15
+ padding: 5px 10px;
16
+ color: $white;
17
+ background-color: $popup-content-bg;
18
+ border: none;
19
+ border-radius: $bdl-border-radius-size;
20
+ box-shadow: none;
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,74 @@
1
+ import React from 'react';
2
+ import noop from 'lodash/noop';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import messages from './messages';
5
+ import PopupBase from './PopupBase';
6
+ import useOutsideEvent from '../../common/useOutsideEvent';
7
+ import { Options } from './Popper';
8
+ import { Shape } from '../../@types/model';
9
+ import './PopupHighlightError.scss';
10
+
11
+ export type Props = {
12
+ onCancel?: () => void;
13
+ shape: Shape;
14
+ };
15
+
16
+ const options: Partial<Options> = {
17
+ modifiers: [
18
+ {
19
+ name: 'arrow',
20
+ options: {
21
+ element: '.ba-Popup-arrow',
22
+ },
23
+ },
24
+ {
25
+ name: 'eventListeners',
26
+ options: {
27
+ scroll: false,
28
+ },
29
+ },
30
+ {
31
+ name: 'offset',
32
+ options: {
33
+ offset: [0, 8],
34
+ },
35
+ },
36
+ {
37
+ name: 'preventOverflow',
38
+ options: {
39
+ padding: 5,
40
+ },
41
+ },
42
+ ],
43
+ placement: 'bottom',
44
+ };
45
+
46
+ export default function PopupHighlightError({ onCancel = noop, shape }: Props): JSX.Element {
47
+ const popupRef = React.useRef<PopupBase>(null);
48
+ const { height, width, x, y } = shape;
49
+
50
+ const reference = {
51
+ getBoundingClientRect: () => ({
52
+ bottom: y + height,
53
+ height,
54
+ left: x,
55
+ right: x + width,
56
+ top: y,
57
+ width,
58
+ }),
59
+ };
60
+
61
+ useOutsideEvent('mousedown', popupRef.current?.popupRef, onCancel);
62
+
63
+ return (
64
+ <PopupBase
65
+ ref={popupRef}
66
+ className="ba-PopupHighlightError"
67
+ data-testid="ba-PopupHighlightError"
68
+ options={options}
69
+ reference={reference}
70
+ >
71
+ <FormattedMessage {...messages.popupHighlightRestrictedPrompt} />
72
+ </PopupBase>
73
+ );
74
+ }
@@ -6,15 +6,12 @@
6
6
  }
7
7
 
8
8
  .ba-Popup-content {
9
- padding-top: 10px;
10
- padding-bottom: 10px;
11
9
  border: solid 1px $bdl-gray-30;
12
10
  border-radius: $bdl-border-radius-size;
13
11
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .05);
14
12
  }
15
13
 
16
14
  .ba-PopupList-prompt {
17
- padding-right: 10px;
18
- padding-left: 10px;
15
+ padding: 10px;
19
16
  }
20
17
  }
@@ -9,11 +9,11 @@ import './PopupList.scss';
9
9
 
10
10
  export type Props<T extends { id: string }> = {
11
11
  activeItemIndex?: number;
12
- itemRowAs?: JSX.Element;
12
+ autoScroll?: boolean;
13
13
  items: T[];
14
14
  onActivate?: (index: number) => void;
15
+ onMouseDown?: (event: React.SyntheticEvent) => void;
15
16
  onSelect: (index: number, event: React.SyntheticEvent) => void;
16
- options?: Partial<Options>;
17
17
  reference: PopupReference;
18
18
  };
19
19
 
@@ -1,7 +1,11 @@
1
- import React from 'react';
1
+ import * as React from 'react';
2
+ import * as Popper from '@popperjs/core';
3
+ import * as ReactRedux from 'react-redux';
2
4
  import PopupBase from './PopupBase';
3
5
  import ReplyForm from '../ReplyForm';
4
-
6
+ import usePrevious from '../../common/usePrevious';
7
+ import { getScale, getRotation } from '../../store/options';
8
+ import { PopupReference } from './Popper';
5
9
  import './PopupReply.scss';
6
10
 
7
11
  export type Props = {
@@ -10,17 +14,22 @@ export type Props = {
10
14
  onCancel: (text?: string) => void;
11
15
  onChange: (text?: string) => void;
12
16
  onSubmit: (text: string) => void;
13
- reference: Element;
17
+ reference: PopupReference;
14
18
  value?: string;
15
19
  };
16
20
 
17
- export const options = {
21
+ const isIE = (): boolean => {
22
+ const { userAgent } = navigator;
23
+ return userAgent.indexOf('Trident/') > 0;
24
+ };
25
+
26
+ export const options: Pick<Popper.Options, 'modifiers'> = {
18
27
  modifiers: [
19
28
  {
20
29
  name: 'arrow',
21
30
  options: {
22
31
  element: '.ba-Popup-arrow',
23
- padding: 20,
32
+ padding: 10,
24
33
  },
25
34
  },
26
35
  {
@@ -45,6 +54,27 @@ export const options = {
45
54
  ],
46
55
  };
47
56
 
57
+ const getOptions = (): Partial<Popper.Options> => {
58
+ const { modifiers: defaultModifiers } = options;
59
+ const placement = isIE() ? 'top' : 'bottom';
60
+ const modifiers = isIE()
61
+ ? [
62
+ ...defaultModifiers,
63
+ {
64
+ name: 'eventListeners',
65
+ options: {
66
+ scroll: false,
67
+ },
68
+ },
69
+ ]
70
+ : defaultModifiers;
71
+
72
+ return {
73
+ modifiers,
74
+ placement,
75
+ };
76
+ };
77
+
48
78
  export default function PopupReply({
49
79
  isPending,
50
80
  onCancel,
@@ -53,8 +83,23 @@ export default function PopupReply({
53
83
  value = '',
54
84
  ...rest
55
85
  }: Props): JSX.Element {
86
+ const popupRef = React.useRef<PopupBase>(null);
87
+ const popupOptions = React.useRef<Partial<Popper.Options>>(getOptions()); // Keep the options reference the same between renders
88
+ const rotation = ReactRedux.useSelector(getRotation);
89
+ const prevRotation = usePrevious(rotation);
90
+ const scale = ReactRedux.useSelector(getScale);
91
+ const prevScale = usePrevious(scale);
92
+
93
+ React.useEffect(() => {
94
+ const { current: popup } = popupRef;
95
+
96
+ if (popup?.popper && (rotation !== prevRotation || scale !== prevScale)) {
97
+ popup.popper.update();
98
+ }
99
+ }, [popupRef, prevRotation, prevScale, rotation, scale]);
100
+
56
101
  return (
57
- <PopupBase options={options} {...rest}>
102
+ <PopupBase ref={popupRef} data-resin-component="popupReply" options={popupOptions.current} {...rest}>
58
103
  <ReplyForm
59
104
  isPending={isPending}
60
105
  onCancel={onCancel}
@@ -11,11 +11,41 @@ export default defineMessages({
11
11
  description: 'Button label for creating a description, comment, or reply',
12
12
  defaultMessage: 'Post',
13
13
  },
14
+ drawingButtonAddComment: {
15
+ id: 'ba.popups.drawing.addComment',
16
+ description: 'Button label for adding a comment in the drawing toolbar',
17
+ defaultMessage: 'Add Comment',
18
+ },
19
+ drawingButtonDelete: {
20
+ id: 'ba.popups.drawing.delete',
21
+ description: 'Button title for deleting a staged drawing',
22
+ defaultMessage: 'Delete',
23
+ },
24
+ drawingButtonRedo: {
25
+ id: 'ba.popups.drawing.redo',
26
+ description: 'Button title for redoing a staged drawing',
27
+ defaultMessage: 'Redo',
28
+ },
29
+ drawingButtonUndo: {
30
+ id: 'ba.popups.drawing.undo',
31
+ description: 'Button title for undoing a staged drawing',
32
+ defaultMessage: 'Undo',
33
+ },
14
34
  fieldPlaceholder: {
15
35
  id: 'ba.popups.replyField.placeholder',
16
36
  description: 'Placeholder for reply field editor',
17
37
  defaultMessage: 'Type a comment...',
18
38
  },
39
+ popupHighlightPromoter: {
40
+ id: 'ba.popups.popupHighlight.promoter',
41
+ description: 'Popup message for highlight promoter',
42
+ defaultMessage: 'Highlight and Comment',
43
+ },
44
+ popupHighlightRestrictedPrompt: {
45
+ id: 'ba.popups.popupHighlight.restrictedPrompt',
46
+ description: 'Prompt message when selection crosses multiple pages',
47
+ defaultMessage: 'Comments restricted to single page',
48
+ },
19
49
  popupListPrompt: {
20
50
  id: 'ba.popups.popupList.prompt',
21
51
  description: 'Prompt message for empty popup list',
@@ -1,7 +1,12 @@
1
1
  @import '~box-ui-elements/es/styles/variables';
2
- @import '~draft-js/dist/Draft.css';
3
2
 
4
3
  .ba-ReplyField {
4
+ @import '~draft-js/dist/Draft';
5
+
6
+ .DraftEditor-editorContainer {
7
+ border-left: none;
8
+ }
9
+
5
10
  .public-DraftEditorPlaceholder-hasFocus,
6
11
  .public-DraftEditorPlaceholder-root {
7
12
  margin: 0;
@@ -1,10 +1,10 @@
1
1
  import * as React from 'react';
2
2
  import classnames from 'classnames';
3
+ import debounce from 'lodash/debounce';
3
4
  import {
4
5
  addMention,
5
6
  getActiveMentionForEditorState,
6
- } from 'box-ui-elements/es/components/form-elements/draft-js-mention-selector';
7
- import fuzzySearch from 'box-ui-elements/es/utils/fuzzySearch';
7
+ } from 'box-ui-elements/es/components/form-elements/draft-js-mention-selector/utils';
8
8
  import { DraftHandleValue, Editor, EditorState } from 'draft-js';
9
9
  import PopupList from '../Popups/PopupList';
10
10
  import { Collaborator } from '../../@types';
@@ -24,8 +24,8 @@ export type Props = {
24
24
  collaborators: Collaborator[];
25
25
  cursorPosition: number;
26
26
  editorState: EditorState;
27
+ fetchCollaborators: (searchString: string) => void;
27
28
  isDisabled?: boolean;
28
- itemRowAs?: JSX.Element;
29
29
  onChange: (editorState: EditorState) => void;
30
30
  placeholder?: string;
31
31
  setCursorPosition: (cursorPosition: number) => void;
@@ -33,15 +33,37 @@ export type Props = {
33
33
 
34
34
  export type State = {
35
35
  activeItemIndex: number;
36
+ popupAutoScroll: boolean;
36
37
  popupReference: VirtualElement | null;
37
38
  };
38
39
 
40
+ export const DEFAULT_COLLAB_DEBOUNCE = 500;
41
+
39
42
  export default class ReplyField extends React.Component<Props, State> {
40
43
  static defaultProps = {
41
44
  isDisabled: false,
42
45
  };
43
46
 
44
- state: State = { activeItemIndex: 0, popupReference: null };
47
+ state: State = { activeItemIndex: 0, popupAutoScroll: false, popupReference: null };
48
+
49
+ fetchCollaborators = debounce((editorState: EditorState): void => {
50
+ const { fetchCollaborators } = this.props;
51
+
52
+ const activeMention = getActiveMentionForEditorState(editorState);
53
+ const trimmedQuery = activeMention?.mentionString.trim();
54
+ if (!trimmedQuery) {
55
+ return;
56
+ }
57
+
58
+ fetchCollaborators(trimmedQuery);
59
+ }, DEFAULT_COLLAB_DEBOUNCE);
60
+
61
+ handleResize = debounce(() => this.updatePopupReference(), 100);
62
+
63
+ componentDidMount(): void {
64
+ // The browser’s native zoom doesn’t always trigger Preview’s resize
65
+ window.addEventListener('resize', this.handleResize);
66
+ }
45
67
 
46
68
  componentDidUpdate({ editorState: prevEditorState }: Props): void {
47
69
  const { editorState } = this.props;
@@ -52,36 +74,10 @@ export default class ReplyField extends React.Component<Props, State> {
52
74
  }
53
75
 
54
76
  componentWillUnmount(): void {
77
+ window.removeEventListener('resize', this.handleResize);
55
78
  this.saveCursorPosition();
56
79
  }
57
80
 
58
- getCollaborators = (): Collaborator[] => {
59
- const { collaborators, editorState } = this.props;
60
-
61
- const activeMention = getActiveMentionForEditorState(editorState);
62
- if (!activeMention) {
63
- return [];
64
- }
65
-
66
- const trimmedQuery = activeMention.mentionString.trim();
67
- // fuzzySearch doesn't match anything if query length is less than 2
68
- // Compared to empty list, full list has a better user experience
69
- if (trimmedQuery.length < 2) {
70
- return collaborators;
71
- }
72
-
73
- return collaborators.filter(({ item }) => {
74
- if (!item) {
75
- return false;
76
- }
77
-
78
- const isNameMatch = fuzzySearch(trimmedQuery, item.name, 0);
79
- const isEmailMatch = 'email' in item && fuzzySearch(trimmedQuery, item.email, 0);
80
-
81
- return isNameMatch || isEmailMatch;
82
- });
83
- };
84
-
85
81
  getVirtualElement = (activeMention: Mention): VirtualElement | null => {
86
82
  const selection = window.getSelection();
87
83
  if (!selection?.focusNode) {
@@ -94,7 +90,7 @@ export default class ReplyField extends React.Component<Props, State> {
94
90
 
95
91
  const { mentionString, mentionTrigger } = activeMention;
96
92
  const offset = mentionString.length + mentionTrigger.length;
97
- const mentionStart = currentCursor - offset;
93
+ const mentionStart = Math.max(0, currentCursor - offset);
98
94
 
99
95
  range.setStart(textNode, mentionStart);
100
96
  range.setEnd(textNode, mentionStart);
@@ -129,19 +125,20 @@ export default class ReplyField extends React.Component<Props, State> {
129
125
  event.stopPropagation();
130
126
  };
131
127
 
132
- setPopupListActiveItem = (index: number): void => this.setState({ activeItemIndex: index });
128
+ setPopupListActiveItem = (index: number, autoScroll = false): void =>
129
+ this.setState({ activeItemIndex: index, popupAutoScroll: autoScroll });
133
130
 
134
131
  handleChange = (nextEditorState: EditorState): void => {
135
132
  const { onChange } = this.props;
136
133
 
134
+ this.fetchCollaborators(nextEditorState);
137
135
  onChange(nextEditorState);
138
136
  };
139
137
 
140
138
  handleSelect = (index: number): void => {
141
- const { editorState } = this.props;
139
+ const { collaborators, editorState } = this.props;
142
140
 
143
141
  const activeMention = getActiveMentionForEditorState(editorState);
144
- const collaborators = this.getCollaborators();
145
142
  const editorStateWithLink = addMention(editorState, activeMention, collaborators[index]);
146
143
 
147
144
  this.handleChange(editorStateWithLink);
@@ -162,8 +159,9 @@ export default class ReplyField extends React.Component<Props, State> {
162
159
  };
163
160
 
164
161
  handleArrow = (event: React.KeyboardEvent): number => {
162
+ const { collaborators } = this.props;
165
163
  const { popupReference } = this.state;
166
- const { length } = this.getCollaborators();
164
+ const { length } = collaborators;
167
165
 
168
166
  if (!popupReference || !length) {
169
167
  return 0;
@@ -182,7 +180,7 @@ export default class ReplyField extends React.Component<Props, State> {
182
180
  return;
183
181
  }
184
182
 
185
- this.setPopupListActiveItem(activeItemIndex === 0 ? length - 1 : activeItemIndex - 1);
183
+ this.setPopupListActiveItem(activeItemIndex === 0 ? length - 1 : activeItemIndex - 1, true);
186
184
  };
187
185
 
188
186
  handleDownArrow = (event: React.KeyboardEvent): void => {
@@ -193,12 +191,14 @@ export default class ReplyField extends React.Component<Props, State> {
193
191
  return;
194
192
  }
195
193
 
196
- this.setPopupListActiveItem(activeItemIndex === length - 1 ? 0 : activeItemIndex + 1);
194
+ this.setPopupListActiveItem(activeItemIndex === length - 1 ? 0 : activeItemIndex + 1, true);
197
195
  };
198
196
 
197
+ handleMouseDown = (event: React.SyntheticEvent): void => event.preventDefault(); // prevent the blur event so that Editor can remain focus
198
+
199
199
  render(): JSX.Element {
200
- const { className, editorState, isDisabled, itemRowAs, placeholder, ...rest } = this.props;
201
- const { activeItemIndex, popupReference } = this.state;
200
+ const { className, collaborators, editorState, isDisabled, placeholder, ...rest } = this.props;
201
+ const { activeItemIndex, popupAutoScroll, popupReference } = this.state;
202
202
 
203
203
  return (
204
204
  <div className={classnames(className, 'ba-ReplyField')}>
@@ -218,9 +218,10 @@ export default class ReplyField extends React.Component<Props, State> {
218
218
  {popupReference && (
219
219
  <PopupList
220
220
  activeItemIndex={activeItemIndex}
221
- itemRowAs={itemRowAs}
222
- items={this.getCollaborators()}
221
+ autoScroll={popupAutoScroll}
222
+ items={collaborators}
223
223
  onActivate={this.setPopupListActiveItem}
224
+ onMouseDown={this.handleMouseDown}
224
225
  onSelect={this.handleSelect}
225
226
  reference={popupReference}
226
227
  />
@@ -1,5 +1,5 @@
1
1
  import { connect } from 'react-redux';
2
- import { AppState, getCollaborators, getCreatorCursor, setCursorAction } from '../../store';
2
+ import { AppState, fetchCollaboratorsAction, getCollaborators, getCreatorCursor, setCursorAction } from '../../store';
3
3
  import ReplyField from './ReplyField';
4
4
  import { Collaborator } from '../../@types';
5
5
 
@@ -14,6 +14,7 @@ export const mapStateToProps = (state: AppState): Props => ({
14
14
  });
15
15
 
16
16
  export const mapDispatchToProps = {
17
+ fetchCollaborators: fetchCollaboratorsAction,
17
18
  setCursorPosition: setCursorAction,
18
19
  };
19
20
 
@@ -0,0 +1,38 @@
1
+ import React, { HTMLAttributes } from 'react';
2
+ import classNames from 'classnames';
3
+ import noop from 'lodash/noop';
4
+
5
+ export type Props = {
6
+ children?: React.ReactNode;
7
+ isDisabled?: boolean;
8
+ isPrimary?: boolean;
9
+ onClick?: (event: React.MouseEvent) => void;
10
+ type: 'button' | 'submit' | 'reset' | undefined;
11
+ } & HTMLAttributes<HTMLButtonElement>;
12
+
13
+ export default function ReplyButton({ children, isDisabled, isPrimary, onClick = noop, ...rest }: Props): JSX.Element {
14
+ const handleClick = (event: React.MouseEvent): void => {
15
+ if (isDisabled) {
16
+ event.preventDefault();
17
+ event.stopPropagation();
18
+ return;
19
+ }
20
+
21
+ onClick(event);
22
+ };
23
+
24
+ return (
25
+ <button
26
+ aria-disabled={isDisabled ? true : undefined}
27
+ className={classNames('btn', {
28
+ 'btn-primary': isPrimary,
29
+ 'is-disabled': isDisabled,
30
+ })}
31
+ onClick={handleClick}
32
+ type="button"
33
+ {...rest}
34
+ >
35
+ {children}
36
+ </button>
37
+ );
38
+ }
@@ -0,0 +1,3 @@
1
+ .ba-ReplyForm {
2
+ margin: 0;
3
+ }