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.
- package/README.md +7 -57
- package/dist/annotations.css +1 -1
- package/dist/annotations.js +1 -1
- package/i18n/bn-IN.js +6 -0
- package/i18n/bn-IN.properties +12 -0
- package/i18n/da-DK.js +6 -0
- package/i18n/da-DK.properties +12 -0
- package/i18n/de-DE.js +6 -0
- package/i18n/de-DE.properties +12 -0
- package/i18n/en-AU.js +6 -0
- package/i18n/en-AU.properties +12 -0
- package/i18n/en-CA.js +6 -0
- package/i18n/en-CA.properties +12 -0
- package/i18n/en-GB.js +6 -0
- package/i18n/en-GB.properties +12 -0
- package/i18n/en-US.js +6 -0
- package/i18n/en-US.properties +12 -0
- package/i18n/en-x-pseudo.js +16 -9
- package/i18n/en-x-pseudo.properties +23 -9
- package/i18n/es-419.js +6 -0
- package/i18n/es-419.properties +12 -0
- package/i18n/es-ES.js +6 -0
- package/i18n/es-ES.properties +12 -0
- package/i18n/fi-FI.js +6 -0
- package/i18n/fi-FI.properties +12 -0
- package/i18n/fr-CA.js +6 -0
- package/i18n/fr-CA.properties +12 -0
- package/i18n/fr-FR.js +6 -0
- package/i18n/fr-FR.properties +12 -0
- package/i18n/hi-IN.js +6 -0
- package/i18n/hi-IN.properties +12 -0
- package/i18n/it-IT.js +6 -0
- package/i18n/it-IT.properties +12 -0
- package/i18n/ja-JP.js +6 -0
- package/i18n/ja-JP.properties +12 -0
- package/i18n/json/src/components/Popups/messages.json +1 -1
- package/i18n/ko-KR.js +6 -0
- package/i18n/ko-KR.properties +12 -0
- package/i18n/nb-NO.js +6 -0
- package/i18n/nb-NO.properties +12 -0
- package/i18n/nl-NL.js +6 -0
- package/i18n/nl-NL.properties +12 -0
- package/i18n/pl-PL.js +6 -0
- package/i18n/pl-PL.properties +12 -0
- package/i18n/pt-BR.js +6 -0
- package/i18n/pt-BR.properties +12 -0
- package/i18n/ru-RU.js +6 -0
- package/i18n/ru-RU.properties +12 -0
- package/i18n/sv-SE.js +6 -0
- package/i18n/sv-SE.properties +12 -0
- package/i18n/tr-TR.js +6 -0
- package/i18n/tr-TR.properties +12 -0
- package/i18n/zh-CN.js +6 -0
- package/i18n/zh-CN.properties +12 -0
- package/i18n/zh-TW.js +6 -0
- package/i18n/zh-TW.properties +12 -0
- package/package.json +29 -29
- package/src/@types/events.ts +8 -2
- package/src/@types/global.ts +1 -0
- package/src/@types/model.ts +20 -7
- package/src/BoxAnnotations.ts +16 -2
- package/src/common/BaseAnnotator.scss +0 -1
- package/src/common/BaseAnnotator.ts +125 -19
- package/src/common/BaseManager.ts +67 -4
- package/src/common/DeselectListener.tsx +21 -0
- package/src/common/DeselectManager.tsx +30 -0
- package/src/common/useIsListInteractive.ts +10 -0
- package/src/common/useMountId.ts +14 -0
- package/src/common/useOutsideEvent.ts +9 -4
- package/src/common/usePrevious.ts +13 -0
- package/src/components/ItemList/ItemList.scss +7 -2
- package/src/components/ItemList/ItemList.tsx +45 -26
- package/src/components/ItemList/ItemRow.scss +19 -7
- package/src/components/ItemList/ItemRow.tsx +33 -12
- package/src/components/PointerCapture/PointerCapture.tsx +116 -0
- package/src/components/PointerCapture/index.ts +2 -0
- package/src/components/Popups/Popper.ts +1 -0
- package/src/components/Popups/PopupArrow.scss +91 -0
- package/src/components/Popups/PopupBase.scss +12 -81
- package/src/components/Popups/PopupBase.tsx +0 -1
- package/src/components/Popups/PopupCursor.tsx +1 -1
- package/src/components/Popups/PopupDrawingToolbar.scss +76 -0
- package/src/components/Popups/PopupDrawingToolbar.tsx +109 -0
- package/src/components/Popups/PopupHighlight.scss +39 -0
- package/src/components/Popups/PopupHighlight.tsx +97 -0
- package/src/components/Popups/PopupHighlightError.scss +23 -0
- package/src/components/Popups/PopupHighlightError.tsx +74 -0
- package/src/components/Popups/PopupList.scss +1 -4
- package/src/components/Popups/PopupList.tsx +2 -2
- package/src/components/Popups/PopupReply.tsx +51 -6
- package/src/components/Popups/messages.ts +30 -0
- package/src/components/ReplyField/ReplyField.scss +6 -1
- package/src/components/ReplyField/ReplyField.tsx +43 -42
- package/src/components/ReplyField/ReplyFieldContainer.tsx +2 -1
- package/src/components/ReplyForm/ReplyButton.tsx +38 -0
- package/src/components/ReplyForm/ReplyForm.scss +3 -0
- package/src/components/ReplyForm/ReplyForm.tsx +49 -12
- package/src/components/ReplyForm/ReplyFormContainer.tsx +6 -4
- package/src/constants.js +1 -8
- package/src/document/DocumentAnnotator.scss +15 -0
- package/src/document/DocumentAnnotator.ts +110 -53
- package/src/document/docUtil.ts +61 -0
- package/src/drawing/DecoratedDrawingPath.scss +9 -0
- package/src/drawing/DecoratedDrawingPath.tsx +43 -0
- package/src/drawing/DrawingAnnotations.scss +39 -0
- package/src/drawing/DrawingAnnotations.tsx +157 -0
- package/src/drawing/DrawingAnnotationsContainer.tsx +66 -0
- package/src/drawing/DrawingCreator.scss +9 -0
- package/src/drawing/DrawingCreator.tsx +183 -0
- package/src/drawing/DrawingCursor.ts +23 -0
- package/src/drawing/DrawingList.tsx +60 -0
- package/src/drawing/DrawingManager.tsx +15 -0
- package/src/drawing/DrawingPath.tsx +17 -0
- package/src/drawing/DrawingPathGroup.tsx +52 -0
- package/src/drawing/DrawingSVG.tsx +34 -0
- package/src/drawing/DrawingSVGGroup.tsx +23 -0
- package/src/drawing/DrawingTarget.scss +11 -0
- package/src/drawing/DrawingTarget.tsx +100 -0
- package/src/drawing/SVGFilterContext.js +5 -0
- package/src/drawing/actions.ts +48 -0
- package/src/drawing/drawingUtil.ts +106 -0
- package/src/drawing/index.ts +2 -0
- package/src/highlight/HighlightAnnotations.scss +12 -0
- package/src/highlight/HighlightAnnotations.tsx +144 -0
- package/src/highlight/HighlightCanvas.scss +9 -0
- package/src/highlight/HighlightCanvas.tsx +109 -0
- package/src/highlight/HighlightContainer.tsx +64 -0
- package/src/highlight/HighlightCreator.scss +12 -0
- package/src/highlight/HighlightCreatorManager.ts +79 -0
- package/src/highlight/HighlightList.scss +13 -0
- package/src/highlight/HighlightList.tsx +92 -0
- package/src/highlight/HighlightListener.ts +37 -0
- package/src/highlight/HighlightManager.tsx +15 -0
- package/src/highlight/HighlightSvg.scss +7 -0
- package/src/highlight/HighlightSvg.tsx +18 -0
- package/src/highlight/HighlightTarget.scss +5 -0
- package/src/highlight/HighlightTarget.tsx +106 -0
- package/src/highlight/actions.ts +36 -0
- package/src/highlight/highlightUtil.ts +143 -0
- package/src/highlight/index.ts +4 -0
- package/src/image/ImageAnnotator.scss +20 -0
- package/src/image/ImageAnnotator.ts +137 -0
- package/src/polyfill.ts +19 -0
- package/src/popup/PopupContainer.tsx +50 -0
- package/src/popup/PopupLayer.scss +3 -0
- package/src/popup/PopupLayer.tsx +103 -0
- package/src/popup/PopupManager.tsx +15 -0
- package/src/region/RegionAnnotation.scss +4 -0
- package/src/region/RegionAnnotation.tsx +15 -16
- package/src/region/RegionAnnotations.scss +1 -15
- package/src/region/RegionAnnotations.tsx +14 -115
- package/src/region/RegionAnnotationsContainer.tsx +24 -0
- package/src/region/RegionCreation.scss +17 -0
- package/src/region/RegionCreation.tsx +79 -0
- package/src/region/RegionCreationContainer.tsx +43 -0
- package/src/region/RegionCreationManager.tsx +15 -0
- package/src/region/RegionCreator.scss +9 -2
- package/src/region/RegionCreator.tsx +50 -71
- package/src/region/RegionList.tsx +8 -12
- package/src/region/RegionManager.tsx +7 -39
- package/src/region/RegionRect.scss +41 -9
- package/src/region/RegionRect.tsx +9 -3
- package/src/region/index.ts +1 -0
- package/src/region/regionUtil.ts +1 -8
- package/src/region/transformUtil.ts +28 -0
- package/src/store/annotations/actions.ts +1 -0
- package/src/store/annotations/reducer.ts +9 -2
- package/src/store/annotations/selectors.ts +1 -0
- package/src/store/annotations/types.ts +1 -0
- package/src/store/common/actions.ts +1 -0
- package/src/store/common/reducer.ts +16 -10
- package/src/store/common/selectors.ts +1 -0
- package/src/store/common/types.ts +3 -0
- package/src/store/createRootReducer.ts +4 -0
- package/src/store/creator/actions.ts +2 -0
- package/src/store/creator/reducer.ts +22 -3
- package/src/store/creator/selectors.ts +9 -1
- package/src/store/creator/types.ts +18 -3
- package/src/store/drawing/actions.ts +10 -0
- package/src/store/drawing/index.ts +4 -0
- package/src/store/drawing/reducer.ts +50 -0
- package/src/store/drawing/selectors.ts +9 -0
- package/src/store/drawing/types.ts +7 -0
- package/src/store/eventing/active.ts +4 -2
- package/src/store/eventing/fetch.ts +8 -0
- package/src/store/eventing/init.ts +14 -0
- package/src/store/eventing/middleware.ts +21 -3
- package/src/store/eventing/mode.ts +7 -0
- package/src/store/eventing/staged.ts +56 -0
- package/src/store/eventing/status.ts +19 -0
- package/src/store/highlight/actions.ts +106 -0
- package/src/store/highlight/index.ts +4 -0
- package/src/store/highlight/reducer.ts +37 -0
- package/src/store/highlight/selectors.ts +12 -0
- package/src/store/highlight/types.ts +14 -0
- package/src/store/index.ts +2 -0
- package/src/store/options/actions.ts +2 -0
- package/src/store/options/reducer.ts +17 -1
- package/src/store/options/selectors.ts +8 -0
- package/src/store/options/types.ts +5 -0
- package/src/store/types.ts +5 -1
- package/src/store/users/actions.ts +3 -2
- package/src/utils/resin.ts +22 -0
- package/src/utils/rotate.ts +131 -0
- package/src/utils/scroll.ts +45 -0
- package/src/utils/util.ts +3 -0
- package/src/region/RegionContainer.tsx +0 -49
- package/src/utils/polyfill.js +0 -161
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppStore,
|
|
3
|
+
getIsSelecting,
|
|
4
|
+
SelectionArg as Selection,
|
|
5
|
+
setIsSelectingAction,
|
|
6
|
+
setSelectionAction,
|
|
7
|
+
} from '../store';
|
|
8
|
+
import { Manager, Options as BaseOptions } from '../common/BaseManager';
|
|
9
|
+
import { MOUSE_PRIMARY } from '../constants';
|
|
10
|
+
|
|
11
|
+
export type Options = {
|
|
12
|
+
getSelection: () => Selection | null;
|
|
13
|
+
selectionChangeDelay?: number;
|
|
14
|
+
store: AppStore;
|
|
15
|
+
} & BaseOptions;
|
|
16
|
+
|
|
17
|
+
export default class HighlightCreatorManager implements Manager {
|
|
18
|
+
referenceEl: HTMLElement;
|
|
19
|
+
|
|
20
|
+
getSelection: () => Selection | null;
|
|
21
|
+
|
|
22
|
+
selectionChangeDelay?: number;
|
|
23
|
+
|
|
24
|
+
selectionChangeTimer?: number;
|
|
25
|
+
|
|
26
|
+
store: AppStore;
|
|
27
|
+
|
|
28
|
+
constructor({ getSelection, referenceEl, selectionChangeDelay = 0, store }: Options) {
|
|
29
|
+
this.getSelection = getSelection;
|
|
30
|
+
this.referenceEl = referenceEl;
|
|
31
|
+
this.selectionChangeDelay = selectionChangeDelay;
|
|
32
|
+
this.store = store;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
exists(parentEl: HTMLElement): boolean {
|
|
36
|
+
return parentEl.contains(this.referenceEl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(): void {
|
|
40
|
+
// Clear previous selection
|
|
41
|
+
this.store.dispatch(setSelectionAction(null));
|
|
42
|
+
|
|
43
|
+
this.referenceEl.addEventListener('mousedown', this.handleMouseDown);
|
|
44
|
+
this.referenceEl.addEventListener('mouseup', this.handleMouseUp);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
style(styles: Partial<CSSStyleDeclaration>): CSSStyleDeclaration {
|
|
48
|
+
return Object.assign(this.referenceEl.style, styles);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
destroy(): void {
|
|
52
|
+
clearTimeout(this.selectionChangeTimer);
|
|
53
|
+
|
|
54
|
+
this.referenceEl.removeEventListener('mousedown', this.handleMouseDown);
|
|
55
|
+
this.referenceEl.removeEventListener('mouseup', this.handleMouseUp);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleMouseDown = ({ buttons }: MouseEvent): void => {
|
|
59
|
+
if (buttons !== MOUSE_PRIMARY) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clearTimeout(this.selectionChangeTimer);
|
|
64
|
+
|
|
65
|
+
this.store.dispatch(setIsSelectingAction(true));
|
|
66
|
+
this.store.dispatch(setSelectionAction(null));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
handleMouseUp = (): void => {
|
|
70
|
+
if (!getIsSelecting(this.store.getState())) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.selectionChangeTimer = window.setTimeout(() => {
|
|
75
|
+
this.store.dispatch(setSelectionAction(this.getSelection()));
|
|
76
|
+
this.store.dispatch(setIsSelectingAction(false));
|
|
77
|
+
}, this.selectionChangeDelay);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import HighlightCanvas, { CanvasShape } from './HighlightCanvas';
|
|
4
|
+
import HighlightSvg from './HighlightSvg';
|
|
5
|
+
import HighlightTarget from './HighlightTarget';
|
|
6
|
+
import useIsListInteractive from '../common/useIsListInteractive';
|
|
7
|
+
import { AnnotationHighlight, Rect, TargetHighlight } from '../@types';
|
|
8
|
+
import { checkValue } from '../utils/util';
|
|
9
|
+
import './HighlightList.scss';
|
|
10
|
+
|
|
11
|
+
export type Props = {
|
|
12
|
+
activeId?: string | null;
|
|
13
|
+
annotations: AnnotationHighlight[];
|
|
14
|
+
className?: string;
|
|
15
|
+
onSelect?: (annotationId: string | null) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function filterHighlight({ target }: { target: TargetHighlight }): boolean {
|
|
19
|
+
const { shapes = [] } = target;
|
|
20
|
+
|
|
21
|
+
return shapes.every(
|
|
22
|
+
({ height, width, x, y }: Rect) => checkValue(height) && checkValue(width) && checkValue(x) && checkValue(y),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getHighlightArea(shapes: Rect[]): number {
|
|
27
|
+
return shapes.reduce((area, { height, width }) => area + height * width, 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sortHighlight(
|
|
31
|
+
{ target: targetA }: { target: TargetHighlight },
|
|
32
|
+
{ target: targetB }: { target: TargetHighlight },
|
|
33
|
+
): number {
|
|
34
|
+
const { shapes: shapesA } = targetA;
|
|
35
|
+
const { shapes: shapesB } = targetB;
|
|
36
|
+
|
|
37
|
+
// Render the smallest highlights last to ensure they are always clickable
|
|
38
|
+
return getHighlightArea(shapesA) > getHighlightArea(shapesB) ? -1 : 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getHighlightShapesFromAnnotations(
|
|
42
|
+
annotations: AnnotationHighlight[],
|
|
43
|
+
activeId: string | null,
|
|
44
|
+
hoverId: string | null,
|
|
45
|
+
): CanvasShape[] {
|
|
46
|
+
return annotations.reduce<CanvasShape[]>((currentShapes, annotation) => {
|
|
47
|
+
const {
|
|
48
|
+
id,
|
|
49
|
+
target: { shapes },
|
|
50
|
+
} = annotation;
|
|
51
|
+
|
|
52
|
+
return currentShapes.concat(
|
|
53
|
+
shapes.map(shape => ({
|
|
54
|
+
...shape,
|
|
55
|
+
isActive: activeId === id,
|
|
56
|
+
isHover: hoverId === id,
|
|
57
|
+
})),
|
|
58
|
+
);
|
|
59
|
+
}, []);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function HighlightList({ activeId = null, annotations, className, onSelect }: Props): JSX.Element {
|
|
63
|
+
const [hoverId, setHoverId] = React.useState<string | null>(null);
|
|
64
|
+
const svgElRef = React.createRef<SVGSVGElement>();
|
|
65
|
+
const isListening = useIsListInteractive();
|
|
66
|
+
const sortedAnnotations = annotations.filter(filterHighlight).sort(sortHighlight);
|
|
67
|
+
const canvasShapes = getHighlightShapesFromAnnotations(sortedAnnotations, activeId, hoverId);
|
|
68
|
+
|
|
69
|
+
const handleTargetHover = (annotationId: string | null): void => {
|
|
70
|
+
setHoverId(annotationId);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={classNames('ba-HighlightList', className)} data-resin-component="highlightList">
|
|
75
|
+
<HighlightCanvas shapes={canvasShapes} />
|
|
76
|
+
<HighlightSvg ref={svgElRef} className={classNames({ 'is-listening': isListening })}>
|
|
77
|
+
{sortedAnnotations.map(({ id, target }) => (
|
|
78
|
+
<HighlightTarget
|
|
79
|
+
key={id}
|
|
80
|
+
annotationId={id}
|
|
81
|
+
isActive={activeId === id}
|
|
82
|
+
onHover={handleTargetHover}
|
|
83
|
+
onSelect={onSelect}
|
|
84
|
+
shapes={target.shapes}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
</HighlightSvg>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default React.memo(HighlightList);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import debounce from 'lodash/debounce';
|
|
2
|
+
import { AppStore, getIsSelecting, SelectionArg as Selection, setSelectionAction } from '../store';
|
|
3
|
+
|
|
4
|
+
export type Options = {
|
|
5
|
+
getSelection: () => Selection | null;
|
|
6
|
+
store: AppStore;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Debounce 500ms for keyboard selection
|
|
10
|
+
const SELECTION_CHANGE_DEBOUNCE = 500;
|
|
11
|
+
|
|
12
|
+
export default class HighlightListener {
|
|
13
|
+
getSelection: () => Selection | null;
|
|
14
|
+
|
|
15
|
+
store: AppStore;
|
|
16
|
+
|
|
17
|
+
constructor({ getSelection, store }: Options) {
|
|
18
|
+
this.getSelection = getSelection;
|
|
19
|
+
this.store = store;
|
|
20
|
+
|
|
21
|
+
document.addEventListener('selectionchange', this.debounceHandleSelectionChange);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
destroy(): void {
|
|
25
|
+
document.removeEventListener('selectionchange', this.debounceHandleSelectionChange);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleSelectionChange = (): void => {
|
|
29
|
+
if (getIsSelecting(this.store.getState())) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.store.dispatch(setSelectionAction(this.getSelection()));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
debounceHandleSelectionChange = debounce(this.handleSelectionChange, SELECTION_CHANGE_DEBOUNCE);
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as ReactDOM from 'react-dom';
|
|
3
|
+
import BaseManager, { Props } from '../common/BaseManager';
|
|
4
|
+
import HighlightContainer from './HighlightContainer';
|
|
5
|
+
|
|
6
|
+
export default class HighlightManager extends BaseManager {
|
|
7
|
+
decorate(): void {
|
|
8
|
+
this.reactEl.classList.add('ba-Layer--highlight');
|
|
9
|
+
this.reactEl.dataset.testid = 'ba-Layer--highlight';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render(props: Props): void {
|
|
13
|
+
ReactDOM.render(<HighlightContainer location={this.location} {...props} />, this.reactEl);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import './HighlightSvg.scss';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const HighlightSvg = ({ children, className }: Props, ref: React.Ref<SVGSVGElement>): JSX.Element => {
|
|
11
|
+
return (
|
|
12
|
+
<svg ref={ref} className={classNames('ba-HighlightSvg', className)}>
|
|
13
|
+
{children}
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default React.forwardRef(HighlightSvg);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import noop from 'lodash/noop';
|
|
4
|
+
import useMountId from '../common/useMountId';
|
|
5
|
+
import { MOUSE_PRIMARY } from '../constants';
|
|
6
|
+
import { Rect } from '../@types/model';
|
|
7
|
+
import './HighlightTarget.scss';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
annotationId: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
isActive?: boolean;
|
|
13
|
+
onHover?: (annotationId: string | null) => void;
|
|
14
|
+
onMount?: (uuid: string) => void;
|
|
15
|
+
onSelect?: (annotationId: string) => void;
|
|
16
|
+
shapes: Array<Rect>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type HighlightTargetRef = HTMLAnchorElement;
|
|
20
|
+
|
|
21
|
+
const HighlightTarget = (props: Props, ref: React.Ref<HighlightTargetRef>): JSX.Element => {
|
|
22
|
+
const { annotationId, className, isActive, onHover = noop, onMount = noop, onSelect = noop, shapes } = props;
|
|
23
|
+
const uuid = useMountId(onMount);
|
|
24
|
+
|
|
25
|
+
const handleClick = (event: React.MouseEvent<HighlightTargetRef>): void => {
|
|
26
|
+
// These are needed to prevent the anchor link from being followed and updating the url location
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
event.stopPropagation();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleFocus = (): void => {
|
|
32
|
+
onSelect(annotationId);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleMouseEnter = (): void => {
|
|
36
|
+
onHover(annotationId);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleMouseLeave = (): void => {
|
|
40
|
+
onHover(null);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleMouseDown = (event: React.MouseEvent<HighlightTargetRef>): void => {
|
|
44
|
+
if (event.buttons !== MOUSE_PRIMARY) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const activeElement = document.activeElement as HTMLElement;
|
|
48
|
+
|
|
49
|
+
event.preventDefault(); // Prevents focus from leaving the anchor immediately in some browsers
|
|
50
|
+
event.stopPropagation(); // Prevents document-level deselect event handlers from executing
|
|
51
|
+
|
|
52
|
+
// IE11 won't apply the focus to the SVG anchor, so this workaround attempts to blur the existing active element.
|
|
53
|
+
// If we don't blur it, we cannot re-focus it next time, since it was already focused and never got blurred.
|
|
54
|
+
if (activeElement && activeElement !== event.currentTarget && activeElement.blur) {
|
|
55
|
+
activeElement.blur();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (event.currentTarget.focus) {
|
|
59
|
+
// Buttons do not receive focus in Firefox and Safari on MacOS; triggers handleFocus
|
|
60
|
+
event.currentTarget.focus();
|
|
61
|
+
} else {
|
|
62
|
+
// IE11 does not support focus function; call handleFocus directly
|
|
63
|
+
handleFocus();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
|
69
|
+
<a
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={classNames('ba-HighlightTarget', className, { 'is-active': isActive })}
|
|
72
|
+
data-ba-reference-id={uuid}
|
|
73
|
+
data-resin-itemid={annotationId}
|
|
74
|
+
data-resin-target="highlightText"
|
|
75
|
+
data-testid={`ba-AnnotationTarget-${annotationId}`}
|
|
76
|
+
href="#"
|
|
77
|
+
onClick={handleClick}
|
|
78
|
+
onFocus={handleFocus}
|
|
79
|
+
onMouseDown={handleMouseDown}
|
|
80
|
+
onMouseEnter={handleMouseEnter}
|
|
81
|
+
onMouseLeave={handleMouseLeave}
|
|
82
|
+
role="button"
|
|
83
|
+
tabIndex={0}
|
|
84
|
+
>
|
|
85
|
+
{shapes.map(rect => {
|
|
86
|
+
const { height, width, x, y } = rect;
|
|
87
|
+
return (
|
|
88
|
+
// eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
|
|
89
|
+
<rect
|
|
90
|
+
key={`${annotationId}-${x}-${y}-${width}-${height}`}
|
|
91
|
+
className="ba-HighlightTarget-rect"
|
|
92
|
+
fill="none"
|
|
93
|
+
height={`${height}%`}
|
|
94
|
+
width={`${width}%`}
|
|
95
|
+
x={`${x}%`}
|
|
96
|
+
y={`${y}%`}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</a>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export { HighlightTarget as HighlightTargetComponent };
|
|
105
|
+
|
|
106
|
+
export default React.forwardRef(HighlightTarget);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AppThunkDispatch, AppState } from '../store';
|
|
2
|
+
import { createAnnotationAction } from '../store/annotations';
|
|
3
|
+
import { getFileVersionId } from '../store/options';
|
|
4
|
+
import { Rect } from '../@types';
|
|
5
|
+
|
|
6
|
+
export type CreateArg = {
|
|
7
|
+
location: number;
|
|
8
|
+
message: string;
|
|
9
|
+
shapes: Rect[];
|
|
10
|
+
text?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const createHighlightAction = (arg: CreateArg) => (dispatch: AppThunkDispatch, getState: () => AppState) => {
|
|
14
|
+
const { location, message, shapes } = arg;
|
|
15
|
+
const state = getState();
|
|
16
|
+
const newAnnotation = {
|
|
17
|
+
description: {
|
|
18
|
+
message,
|
|
19
|
+
type: 'reply' as const,
|
|
20
|
+
},
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/camelcase
|
|
22
|
+
file_version: {
|
|
23
|
+
id: getFileVersionId(state),
|
|
24
|
+
},
|
|
25
|
+
target: {
|
|
26
|
+
location: {
|
|
27
|
+
type: 'page' as const,
|
|
28
|
+
value: location,
|
|
29
|
+
},
|
|
30
|
+
shapes,
|
|
31
|
+
type: 'highlight' as const,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return dispatch(createAnnotationAction(newAnnotation));
|
|
36
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Annotation, AnnotationHighlight, Position, Shape, Type } from '../@types';
|
|
2
|
+
|
|
3
|
+
export const getBoundingRect = (shapes: Shape[]): Shape => {
|
|
4
|
+
let minX = Number.MAX_VALUE;
|
|
5
|
+
let minY = Number.MAX_VALUE;
|
|
6
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
7
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
8
|
+
|
|
9
|
+
shapes.forEach(({ height, width, x, y }) => {
|
|
10
|
+
const x2 = x + width;
|
|
11
|
+
const y2 = y + height;
|
|
12
|
+
|
|
13
|
+
if (x < minX) {
|
|
14
|
+
minX = x;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (y < minY) {
|
|
18
|
+
minY = y;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (x2 > maxX) {
|
|
22
|
+
maxX = x2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (y2 > maxY) {
|
|
26
|
+
maxY = y2;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
x: minX,
|
|
32
|
+
y: minY,
|
|
33
|
+
width: maxX - minX,
|
|
34
|
+
height: maxY - minY,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const centerShape = (shape: Shape): Position => {
|
|
39
|
+
const { height, width } = shape;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
x: width / 2,
|
|
43
|
+
y: height / 2,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const centerHighlight = (shapes: Shape[]): Position => {
|
|
48
|
+
const boundingShape = getBoundingRect(shapes);
|
|
49
|
+
const { x: shapeX, y: shapeY } = boundingShape;
|
|
50
|
+
const { x: centerX, y: centerY } = centerShape(boundingShape);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
x: centerX + shapeX,
|
|
54
|
+
y: centerY + shapeY,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function isHighlight(annotation: Annotation): annotation is AnnotationHighlight {
|
|
59
|
+
return annotation?.target?.type === Type.highlight;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const getThreshold = (prev: number, curr: number, threshold: number): number =>
|
|
63
|
+
Math.max(Math.round(prev * threshold), Math.round(curr * threshold), 1);
|
|
64
|
+
|
|
65
|
+
export const dedupeRects = (rects: Shape[], threshold = 0.1): Shape[] => {
|
|
66
|
+
const dedupedRects: Shape[] = [];
|
|
67
|
+
|
|
68
|
+
rects.forEach(curr => {
|
|
69
|
+
const prev = dedupedRects.pop();
|
|
70
|
+
// empty list, push current
|
|
71
|
+
if (!prev) {
|
|
72
|
+
dedupedRects.push(curr);
|
|
73
|
+
// The return in forEach callback is acting like a continue in for loop
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { width: prevWidth, height: prevHeight, x: prevX, y: prevY } = prev;
|
|
78
|
+
const { width, height, x, y } = curr;
|
|
79
|
+
|
|
80
|
+
const xThreshold = getThreshold(prevWidth, width, threshold);
|
|
81
|
+
const yThreshold = getThreshold(prevHeight, height, threshold);
|
|
82
|
+
|
|
83
|
+
if (Math.abs(prevX - x) <= xThreshold && Math.abs(prevY - y) <= yThreshold) {
|
|
84
|
+
// the same rect, push the larger one
|
|
85
|
+
dedupedRects.push(prevHeight * prevWidth > height * width ? prev : curr);
|
|
86
|
+
} else {
|
|
87
|
+
// different rects, push both
|
|
88
|
+
dedupedRects.push(prev);
|
|
89
|
+
dedupedRects.push(curr);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return dedupedRects;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const combineRects = (rects: Shape[]): Shape[] => {
|
|
97
|
+
const result: Shape[] = [];
|
|
98
|
+
|
|
99
|
+
dedupeRects(rects).forEach(rect => {
|
|
100
|
+
const { height, width, x, y } = rect;
|
|
101
|
+
const lastRect = result.pop();
|
|
102
|
+
|
|
103
|
+
if (!lastRect) {
|
|
104
|
+
result.push(rect);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// This algorithm employs a hueristic that assumes rects are returned in ascending y coordinate
|
|
109
|
+
// followed by ascending x coordinate.
|
|
110
|
+
// If there is already a previous rect, check first to see if the current rect is approximately
|
|
111
|
+
// close enough (in both the x and y coordinate) to be considered as part of the same rect
|
|
112
|
+
// If it is not close enough then add the current rect as a separate rect in the array
|
|
113
|
+
const { height: lastHeight, width: lastWidth, x: lastX, y: lastY } = lastRect;
|
|
114
|
+
const lastMaxX = lastX + lastWidth;
|
|
115
|
+
const thresholdX = getThreshold(lastWidth, width, 0.3);
|
|
116
|
+
const thresholdY = getThreshold(lastHeight, height, 0.6);
|
|
117
|
+
const isXCloseEnough = Math.abs(x - lastMaxX) < thresholdX;
|
|
118
|
+
const isYCloseEnough = Math.abs(y - lastY) < thresholdY;
|
|
119
|
+
|
|
120
|
+
// If rect is close enough, add the rect to the existing entry, otherwise add it as a new entry
|
|
121
|
+
if (isXCloseEnough && isYCloseEnough) {
|
|
122
|
+
result.push(getBoundingRect([lastRect, rect]));
|
|
123
|
+
} else {
|
|
124
|
+
result.push(lastRect);
|
|
125
|
+
result.push(rect);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const getShapeRelativeToContainer = (shape: Shape, containerShape: Shape): Shape => {
|
|
133
|
+
const { height, width, x, y } = shape;
|
|
134
|
+
const { height: containerHeight, width: containerWidth, x: containerX, y: containerY } = containerShape;
|
|
135
|
+
|
|
136
|
+
// If for some reason the x or y turns out to be negative in relation to the container, assume 0 instead
|
|
137
|
+
return {
|
|
138
|
+
height: (height / containerHeight) * 100,
|
|
139
|
+
width: (width / containerWidth) * 100,
|
|
140
|
+
x: (Math.max(0, x - containerX) / containerWidth) * 100,
|
|
141
|
+
y: (Math.max(0, y - containerY) / containerHeight) * 100,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.bp-image {
|
|
2
|
+
.ba-Layer {
|
|
3
|
+
position: absolute;
|
|
4
|
+
white-space: initial; // Overrides white-space: no-wrap inherited .bp-image
|
|
5
|
+
text-align: initial; // Overrides text-align: center inherited .bp-image
|
|
6
|
+
pointer-events: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Applied to img element, which is managed by Preview SDK
|
|
10
|
+
.ba-is-drawing {
|
|
11
|
+
user-select: none; // Prevent visible selection highlight
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Raise the rendering order to override the stacking context created by the
|
|
15
|
+
// transform applied to each of the managers' react mount element to account for
|
|
16
|
+
// rotation
|
|
17
|
+
.ba-Layer--popup {
|
|
18
|
+
z-index: 1;
|
|
19
|
+
}
|
|
20
|
+
}
|