box-annotations 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/annotations.css +1 -1
- package/dist/annotations.js +1 -1
- package/package.json +1 -1
- package/src/@types/model.ts +12 -4
- package/src/BoxAnnotations.ts +7 -0
- package/src/common/BaseManager.ts +10 -2
- package/src/constants.ts +23 -0
- package/src/drawing/DrawingAnnotations.tsx +39 -2
- package/src/drawing/DrawingManager.tsx +1 -1
- package/src/drawing/actions.ts +4 -2
- package/src/highlight/actions.ts +2 -1
- package/src/media/MediaAnnotator.scss +20 -0
- package/src/media/MediaAnnotator.ts +140 -0
- package/src/popup/PopupLayer.tsx +7 -3
- package/src/popup/PopupManager.tsx +2 -2
- package/src/region/RegionAnnotations.tsx +24 -3
- package/src/region/RegionCreation.tsx +8 -2
- package/src/region/RegionCreationManager.tsx +1 -1
- package/src/region/RegionManager.tsx +1 -1
- package/src/region/actions.ts +4 -2
- package/src/store/annotations/selectors.ts +10 -2
- package/src/store/creator/selectors.ts +13 -0
- package/src/test-utils/videoAnnotations.tsx +178 -0
- package/src/utils/useVideoTiming.ts +112 -0
- package/src/constants.js +0 -8
package/package.json
CHANGED
package/src/@types/model.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-use-before-define */
|
|
2
2
|
import { Permissions } from './api';
|
|
3
|
+
import { TARGET_TYPE } from '../constants';
|
|
3
4
|
|
|
4
5
|
// New Data Model Types
|
|
5
6
|
export interface Annotation {
|
|
@@ -37,10 +38,17 @@ export type Dimensions = {
|
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export interface Page {
|
|
40
|
-
type:
|
|
41
|
+
type: TARGET_TYPE.PAGE;
|
|
41
42
|
value: number;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export interface Frame {
|
|
46
|
+
type: TARGET_TYPE.FRAME;
|
|
47
|
+
value: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Location = Page | Frame;
|
|
51
|
+
|
|
44
52
|
export interface Path {
|
|
45
53
|
clientId?: string;
|
|
46
54
|
points: Array<Position>;
|
|
@@ -93,7 +101,7 @@ export interface Stroke {
|
|
|
93
101
|
export type Target = TargetDrawing | TargetHighlight | TargetPoint | TargetRegion;
|
|
94
102
|
|
|
95
103
|
export interface TargetDrawing {
|
|
96
|
-
location:
|
|
104
|
+
location: Location;
|
|
97
105
|
path_groups: Array<PathGroup>;
|
|
98
106
|
type: 'drawing';
|
|
99
107
|
}
|
|
@@ -106,12 +114,12 @@ export interface TargetHighlight {
|
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
export interface TargetPoint extends Position {
|
|
109
|
-
location:
|
|
117
|
+
location: Location;
|
|
110
118
|
type: 'point';
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
export interface TargetRegion {
|
|
114
|
-
location:
|
|
122
|
+
location: Location;
|
|
115
123
|
shape: Rect;
|
|
116
124
|
type: 'region';
|
|
117
125
|
}
|
package/src/BoxAnnotations.ts
CHANGED
|
@@ -4,6 +4,7 @@ import BaseAnnotator from './common/BaseAnnotator';
|
|
|
4
4
|
import ImageAnnotator from './image/ImageAnnotator';
|
|
5
5
|
import DocumentAnnotator from './document/DocumentAnnotator';
|
|
6
6
|
import { IntlOptions, Permissions, PERMISSIONS, Type } from './@types';
|
|
7
|
+
import MediaAnnotator from './media/MediaAnnotator';
|
|
7
8
|
|
|
8
9
|
type Annotator = {
|
|
9
10
|
CONSTRUCTOR: typeof BaseAnnotator;
|
|
@@ -60,6 +61,12 @@ const ANNOTATORS: Annotator[] = [
|
|
|
60
61
|
TYPES: [Type.region],
|
|
61
62
|
VIEWERS: ['Image'],
|
|
62
63
|
},
|
|
64
|
+
{
|
|
65
|
+
CONSTRUCTOR: MediaAnnotator,
|
|
66
|
+
NAME: 'Media',
|
|
67
|
+
TYPES: [Type.region],
|
|
68
|
+
VIEWERS: ['Dash','MP4'],
|
|
69
|
+
},
|
|
63
70
|
];
|
|
64
71
|
|
|
65
72
|
class BoxAnnotations {
|
|
@@ -2,13 +2,15 @@ import * as ReactDOM from 'react-dom/client';
|
|
|
2
2
|
import { IntlShape } from 'react-intl';
|
|
3
3
|
import { Store } from 'redux';
|
|
4
4
|
import { applyResinTags } from '../utils/resin';
|
|
5
|
+
import { TARGET_TYPE } from '../constants';
|
|
5
6
|
|
|
6
7
|
export type ResinTags = Record<string, unknown>;
|
|
7
8
|
|
|
8
9
|
export type Options = {
|
|
9
10
|
location?: number;
|
|
10
11
|
referenceEl: HTMLElement;
|
|
11
|
-
resinTags?:
|
|
12
|
+
resinTags?: Record<string, unknown>;
|
|
13
|
+
targetType?: TARGET_TYPE;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export type Props = {
|
|
@@ -30,13 +32,19 @@ export default class BaseManager implements Manager {
|
|
|
30
32
|
|
|
31
33
|
root: ReactDOM.Root | null = null;
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
targetType: TARGET_TYPE;
|
|
36
|
+
|
|
37
|
+
referenceEl: HTMLElement;
|
|
38
|
+
|
|
39
|
+
constructor({ location = 1, referenceEl, resinTags = {}, targetType = TARGET_TYPE.PAGE }: Options) {
|
|
34
40
|
this.location = location;
|
|
35
41
|
this.reactEl = this.insert(referenceEl, {
|
|
36
42
|
...resinTags,
|
|
37
43
|
feature: 'annotations',
|
|
38
44
|
});
|
|
39
45
|
|
|
46
|
+
this.referenceEl = referenceEl;
|
|
47
|
+
this.targetType = targetType;
|
|
40
48
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
41
49
|
|
|
42
50
|
this.decorate();
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const ANNOTATOR_EVENT = {
|
|
2
|
+
fetch: 'annotationsfetched',
|
|
3
|
+
error: 'annotationerror',
|
|
4
|
+
scale: 'scaleannotations',
|
|
5
|
+
setVisibility: 'annotationsetvisibility',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const MOUSE_PRIMARY = 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This is the default location index for video annotations.
|
|
12
|
+
* Documents have locations that represent a page but that doesn't apply to
|
|
13
|
+
* videos as they have no pages. The annnotations managers take locations as
|
|
14
|
+
* parameters so we need to provide a default value that indicates that this is a
|
|
15
|
+
* video annotation.
|
|
16
|
+
*/
|
|
17
|
+
export const MEDIA_LOCATION_INDEX = -1;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export enum TARGET_TYPE {
|
|
21
|
+
FRAME = 'frame',
|
|
22
|
+
PAGE = 'page',
|
|
23
|
+
}
|
|
@@ -10,6 +10,8 @@ import PopupDrawingToolbar, { PopupBaseRef } from '../components/Popups/PopupDra
|
|
|
10
10
|
import { AnnotationDrawing, PathGroup } from '../@types';
|
|
11
11
|
import { CreatorItemDrawing, CreatorStatus } from '../store';
|
|
12
12
|
import './DrawingAnnotations.scss';
|
|
13
|
+
import { TARGET_TYPE } from '../constants';
|
|
14
|
+
import useVideoTiming from '../utils/useVideoTiming';
|
|
13
15
|
|
|
14
16
|
export type Props = {
|
|
15
17
|
activeAnnotationId: string | null;
|
|
@@ -29,6 +31,8 @@ export type Props = {
|
|
|
29
31
|
setupDrawing: (location: number) => void;
|
|
30
32
|
stashedPathGroups: Array<PathGroup>;
|
|
31
33
|
undoDrawingPathGroup: () => void;
|
|
34
|
+
targetType: TARGET_TYPE;
|
|
35
|
+
referenceEl?: HTMLElement;
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
@@ -50,6 +54,8 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
50
54
|
setupDrawing,
|
|
51
55
|
stashedPathGroups,
|
|
52
56
|
undoDrawingPathGroup,
|
|
57
|
+
targetType,
|
|
58
|
+
referenceEl,
|
|
53
59
|
} = props;
|
|
54
60
|
const [isDrawing, setIsDrawing] = React.useState<boolean>(false);
|
|
55
61
|
const [stagedRootEl, setStagedRootEl] = React.useState<DrawingSVGRef | null>(null);
|
|
@@ -60,6 +66,13 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
60
66
|
const stagedGroupRef = React.useRef<SVGGElement>(null);
|
|
61
67
|
const { current: drawingSVGGroup } = stagedGroupRef;
|
|
62
68
|
|
|
69
|
+
const { isVideoSeeking, getCurrentVideoLocation } = useVideoTiming({
|
|
70
|
+
targetType,
|
|
71
|
+
referenceEl,
|
|
72
|
+
activeAnnotationId,
|
|
73
|
+
annotations,
|
|
74
|
+
});
|
|
75
|
+
|
|
63
76
|
const handleAnnotationActive = (annotationId: string | null): void => {
|
|
64
77
|
setActiveAnnotationId(annotationId);
|
|
65
78
|
};
|
|
@@ -72,8 +85,16 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
72
85
|
const handleRedo = (): void => {
|
|
73
86
|
redoDrawingPathGroup();
|
|
74
87
|
};
|
|
88
|
+
|
|
89
|
+
const getCurrentLocation = (): number => {
|
|
90
|
+
if (targetType === TARGET_TYPE.FRAME) {
|
|
91
|
+
return getCurrentVideoLocation();
|
|
92
|
+
}
|
|
93
|
+
return location;
|
|
94
|
+
};
|
|
75
95
|
const handleReply = (): void => {
|
|
76
|
-
|
|
96
|
+
const annotationLocation = getCurrentLocation();
|
|
97
|
+
setStaged({ location: annotationLocation, pathGroups: drawnPathGroups });
|
|
77
98
|
setStatus(CreatorStatus.staged);
|
|
78
99
|
};
|
|
79
100
|
const handleStart = (): void => {
|
|
@@ -95,11 +116,27 @@ const DrawingAnnotations = (props: Props): JSX.Element => {
|
|
|
95
116
|
}
|
|
96
117
|
}, [drawnPathGroups]);
|
|
97
118
|
|
|
119
|
+
|
|
120
|
+
let annotationsToShow: AnnotationDrawing[] = [];
|
|
121
|
+
// For video annotations, we only show the active annotation and we wait for the video
|
|
122
|
+
// to seek to the annotation location before showing it. This prevents annoations from being
|
|
123
|
+
// shown on the incorrect frame while the video is seeking.
|
|
124
|
+
if (targetType === TARGET_TYPE.FRAME ) {
|
|
125
|
+
if (!isVideoSeeking && activeAnnotationId) {
|
|
126
|
+
annotationsToShow = annotations.filter(annotation => annotation.id === activeAnnotationId);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
annotationsToShow = annotations;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
98
135
|
return (
|
|
99
136
|
<>
|
|
100
137
|
<DrawingList
|
|
101
138
|
activeId={activeAnnotationId}
|
|
102
|
-
annotations={
|
|
139
|
+
annotations={annotationsToShow}
|
|
103
140
|
className="ba-DrawingAnnotations-list"
|
|
104
141
|
onSelect={handleAnnotationActive}
|
|
105
142
|
/>
|
|
@@ -14,6 +14,6 @@ export default class DrawingListManager extends BaseManager {
|
|
|
14
14
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
this.root.render(<DrawingAnnotationsContainer location={this.location} {...props} />);
|
|
17
|
+
this.root.render(<DrawingAnnotationsContainer referenceEl={this.referenceEl} targetType={this.targetType} location={this.location} {...props} />);
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/drawing/actions.ts
CHANGED
|
@@ -2,16 +2,18 @@ import { AppThunkDispatch, AppState, getCreatorStatus, CreatorStatus } from '../
|
|
|
2
2
|
import { createAnnotationAction } from '../store/annotations';
|
|
3
3
|
import { getFileVersionId } from '../store/options';
|
|
4
4
|
import { PathGroup } from '../@types';
|
|
5
|
+
import { TARGET_TYPE } from '../constants';
|
|
5
6
|
import { resetDrawingAction, setDrawingLocationAction } from '../store/drawing';
|
|
6
7
|
|
|
7
8
|
export type CreateArg = {
|
|
8
9
|
location: number;
|
|
9
10
|
message: string;
|
|
10
11
|
pathGroups: Array<PathGroup>;
|
|
12
|
+
targetType: TARGET_TYPE;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
export const createDrawingAction = (arg: CreateArg) => (dispatch: AppThunkDispatch, getState: () => AppState) => {
|
|
14
|
-
const { location, message, pathGroups } = arg;
|
|
16
|
+
const { location, message, pathGroups, targetType } = arg;
|
|
15
17
|
const state = getState();
|
|
16
18
|
const newAnnotation = {
|
|
17
19
|
description: {
|
|
@@ -23,7 +25,7 @@ export const createDrawingAction = (arg: CreateArg) => (dispatch: AppThunkDispat
|
|
|
23
25
|
},
|
|
24
26
|
target: {
|
|
25
27
|
location: {
|
|
26
|
-
type:
|
|
28
|
+
type: targetType,
|
|
27
29
|
value: location,
|
|
28
30
|
},
|
|
29
31
|
path_groups: pathGroups,
|
package/src/highlight/actions.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { AppThunkDispatch, AppState } from '../store';
|
|
|
2
2
|
import { createAnnotationAction } from '../store/annotations';
|
|
3
3
|
import { getFileVersionId } from '../store/options';
|
|
4
4
|
import { Rect } from '../@types';
|
|
5
|
+
import { TARGET_TYPE } from '../constants';
|
|
5
6
|
|
|
6
7
|
export type CreateArg = {
|
|
7
8
|
location: number;
|
|
@@ -23,7 +24,7 @@ export const createHighlightAction = (arg: CreateArg) => (dispatch: AppThunkDisp
|
|
|
23
24
|
},
|
|
24
25
|
target: {
|
|
25
26
|
location: {
|
|
26
|
-
type:
|
|
27
|
+
type: TARGET_TYPE.PAGE as const,
|
|
27
28
|
value: location,
|
|
28
29
|
},
|
|
29
30
|
shapes,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.bp-media {
|
|
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: 4;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Unsubscribe } from 'redux';
|
|
2
|
+
import BaseAnnotator, { Options } from '../common/BaseAnnotator';
|
|
3
|
+
import PopupManager from '../popup/PopupManager';
|
|
4
|
+
import { DrawingManager } from '../drawing';
|
|
5
|
+
import { RegionCreationManager, RegionManager } from '../region';
|
|
6
|
+
import { CreatorStatus, getCreatorStatus } from '../store/creator';
|
|
7
|
+
import { getAnnotation, getFileId, getIsCurrentFileVersion, getRotation } from '../store';
|
|
8
|
+
import { Manager } from '../common/BaseManager';
|
|
9
|
+
import './MediaAnnotator.scss';
|
|
10
|
+
import { MEDIA_LOCATION_INDEX, TARGET_TYPE } from '../constants';
|
|
11
|
+
|
|
12
|
+
export const CSS_IS_DRAWING_CLASS = 'ba-is-drawing';
|
|
13
|
+
export const VIDEO_PARENT_SELECTOR = '.bp-media';
|
|
14
|
+
|
|
15
|
+
export default class MediaAnnotator extends BaseAnnotator {
|
|
16
|
+
annotatedEl?: HTMLElement;
|
|
17
|
+
|
|
18
|
+
managers: Set<Manager> = new Set();
|
|
19
|
+
|
|
20
|
+
storeHandler?: Unsubscribe;
|
|
21
|
+
|
|
22
|
+
constructor(options: Options) {
|
|
23
|
+
super(options);
|
|
24
|
+
|
|
25
|
+
// Subscribe to the store so that the annoator can capture changes to the creator status
|
|
26
|
+
this.storeHandler = this.store.subscribe(this.handleStore);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
destroy(): void {
|
|
30
|
+
|
|
31
|
+
// If the store handler is present, unsubscribe from the store.
|
|
32
|
+
if (this.storeHandler) {
|
|
33
|
+
this.storeHandler();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (this.managers) {
|
|
37
|
+
this.managers.forEach(manager => manager.destroy());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
super.destroy();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getAnnotatedElement(): HTMLElement | null | undefined {
|
|
44
|
+
return this.containerEl?.querySelector(VIDEO_PARENT_SELECTOR);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
addManagers(referenceEl: HTMLVideoElement, resinTags: { fileid: string|null; iscurrent: boolean }): void {
|
|
49
|
+
this.managers.add(new PopupManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
|
|
50
|
+
this.managers.add(new DrawingManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
|
|
51
|
+
this.managers.add(new RegionManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
|
|
52
|
+
this.managers.add(new RegionCreationManager({ location: MEDIA_LOCATION_INDEX, referenceEl, resinTags, targetType: TARGET_TYPE.FRAME }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getManagers(parentEl: HTMLElement, referenceEl: HTMLVideoElement): Set<Manager> {
|
|
56
|
+
const fileId = getFileId(this.store.getState());
|
|
57
|
+
const isCurrentFileVersion = getIsCurrentFileVersion(this.store.getState());
|
|
58
|
+
const resinTags = { fileid: fileId, iscurrent: isCurrentFileVersion };
|
|
59
|
+
this.managers.forEach(manager => {
|
|
60
|
+
if (!manager.exists(parentEl)) {
|
|
61
|
+
manager.destroy();
|
|
62
|
+
this.managers.delete(manager);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (this.managers.size === 0) {
|
|
67
|
+
this.addManagers(referenceEl, resinTags);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return this.managers;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// returns the actual video element
|
|
76
|
+
getReference(): HTMLVideoElement | null | undefined {
|
|
77
|
+
return this.annotatedEl?.querySelector('video');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handleStore = (): void => {
|
|
81
|
+
const referenceEl = this.getReference();
|
|
82
|
+
|
|
83
|
+
if (!referenceEl) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (getCreatorStatus(this.store.getState()) === CreatorStatus.started) {
|
|
88
|
+
referenceEl.classList.add(CSS_IS_DRAWING_CLASS);
|
|
89
|
+
} else {
|
|
90
|
+
referenceEl.classList.remove(CSS_IS_DRAWING_CLASS);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
render(): void {
|
|
95
|
+
const referenceEl = this.getReference();
|
|
96
|
+
const rotation = getRotation(this.store.getState()) || 0;
|
|
97
|
+
|
|
98
|
+
if (!this.annotatedEl || !referenceEl) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.getManagers(this.annotatedEl, referenceEl).forEach(manager => {
|
|
103
|
+
manager.style({
|
|
104
|
+
height: `${referenceEl.offsetHeight}px`,
|
|
105
|
+
left: `${referenceEl.offsetLeft}px`,
|
|
106
|
+
top: `${referenceEl.offsetTop}px`,
|
|
107
|
+
transform: `rotate(${rotation}deg)`,
|
|
108
|
+
width: `${referenceEl.offsetWidth}px`,
|
|
109
|
+
});
|
|
110
|
+
manager.render({
|
|
111
|
+
intl: this.intl,
|
|
112
|
+
store: this.store,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.postRender();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// This function gets called by the client. We are using the name scrollToAnnotation to match
|
|
120
|
+
// the other annotators as the client calls this function when it wants to show an annoation on the
|
|
121
|
+
// preview. The only difference is that we are using the video element's currentTime to scroll to the
|
|
122
|
+
// annotation instead of the scrollLeft and scrollTop of the parent element.
|
|
123
|
+
scrollToAnnotation(annotationId: string | null): void {
|
|
124
|
+
if (!annotationId) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const annotation = getAnnotation(this.store.getState(), annotationId);
|
|
129
|
+
const annotationLocation = annotation?.target.location.value ?? 0
|
|
130
|
+
const video = this.getReference();
|
|
131
|
+
if (!annotation || !video || !this.annotatedEl) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
video.pause();
|
|
136
|
+
video.currentTime = annotationLocation / 1000;
|
|
137
|
+
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
package/src/popup/PopupLayer.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../store';
|
|
15
15
|
import { PopupReference } from '../components/Popups/Popper';
|
|
16
16
|
import './PopupLayer.scss';
|
|
17
|
+
import { TARGET_TYPE } from '../constants';
|
|
17
18
|
|
|
18
19
|
export type Props = {
|
|
19
20
|
createDrawing?: (arg: DrawingCreateArg) => void;
|
|
@@ -28,6 +29,7 @@ export type Props = {
|
|
|
28
29
|
setMessage: (message: string) => void;
|
|
29
30
|
staged?: CreatorItem | null;
|
|
30
31
|
status: CreatorStatus;
|
|
32
|
+
targetType: TARGET_TYPE;
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const modeStagedMap: { [M in Mode]?: (staged: CreatorItem | null) => boolean } = {
|
|
@@ -49,6 +51,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
49
51
|
setMessage,
|
|
50
52
|
staged,
|
|
51
53
|
status,
|
|
54
|
+
targetType,
|
|
52
55
|
} = props;
|
|
53
56
|
|
|
54
57
|
const [reference, setReference] = React.useState<PopupReference | null>(null);
|
|
@@ -70,11 +73,11 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
if (isCreatorStagedHighlight(staged)) {
|
|
73
|
-
createHighlight({ ...staged, message });
|
|
76
|
+
createHighlight({ ...staged, message, targetType });
|
|
74
77
|
} else if (isCreatorStagedRegion(staged)) {
|
|
75
|
-
createRegion({ ...staged, message });
|
|
78
|
+
createRegion({ ...staged, message, targetType });
|
|
76
79
|
} else if (isCreatorStagedDrawing(staged)) {
|
|
77
|
-
createDrawing({ ...staged, message });
|
|
80
|
+
createDrawing({ ...staged, message, targetType });
|
|
78
81
|
}
|
|
79
82
|
};
|
|
80
83
|
|
|
@@ -82,6 +85,7 @@ const PopupLayer = (props: Props): JSX.Element | null => {
|
|
|
82
85
|
setReference(referenceId ? document.querySelector(`[data-ba-reference-id="${referenceId}"]`) : null);
|
|
83
86
|
}, [referenceId]);
|
|
84
87
|
|
|
88
|
+
|
|
85
89
|
return (
|
|
86
90
|
<>
|
|
87
91
|
{canCreate && canReply && reference && staged && (
|
|
@@ -13,7 +13,7 @@ export default class PopupManager extends BaseManager {
|
|
|
13
13
|
if (!this.root) {
|
|
14
14
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
this.root.render(<PopupContainer location={this.location}
|
|
16
|
+
|
|
17
|
+
this.root.render(<PopupContainer location={this.location} {...props} targetType={this.targetType} />);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -2,24 +2,45 @@ import * as React from 'react';
|
|
|
2
2
|
import RegionList from './RegionList';
|
|
3
3
|
import { AnnotationRegion } from '../@types';
|
|
4
4
|
import './RegionAnnotations.scss';
|
|
5
|
+
import useVideoTiming from '../utils/useVideoTiming';
|
|
6
|
+
import { TARGET_TYPE } from '../constants';
|
|
5
7
|
|
|
6
|
-
type Props = {
|
|
8
|
+
export type Props = {
|
|
7
9
|
activeAnnotationId: string | null;
|
|
8
10
|
annotations: AnnotationRegion[];
|
|
9
11
|
setActiveAnnotationId: (annotationId: string | null) => void;
|
|
12
|
+
referenceEl?: HTMLElement;
|
|
13
|
+
targetType: TARGET_TYPE;
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
const RegionAnnotations = (props: Props): JSX.Element => {
|
|
13
|
-
const { activeAnnotationId, annotations, setActiveAnnotationId } = props;
|
|
17
|
+
const { activeAnnotationId, annotations, setActiveAnnotationId, referenceEl, targetType } = props;
|
|
18
|
+
const { isVideoSeeking } = useVideoTiming({
|
|
19
|
+
targetType,
|
|
20
|
+
referenceEl,
|
|
21
|
+
activeAnnotationId: activeAnnotationId || null,
|
|
22
|
+
annotations: annotations || [],
|
|
23
|
+
});
|
|
14
24
|
|
|
15
25
|
const handleAnnotationActive = (annotationId: string | null): void => {
|
|
16
26
|
setActiveAnnotationId(annotationId);
|
|
17
27
|
};
|
|
18
28
|
|
|
29
|
+
let annotationsToShow: AnnotationRegion[] = []
|
|
30
|
+
|
|
31
|
+
if (targetType === TARGET_TYPE.FRAME) {
|
|
32
|
+
if (!isVideoSeeking && activeAnnotationId) {
|
|
33
|
+
annotationsToShow = annotations.filter(annotation => annotation.id === activeAnnotationId);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
annotationsToShow = annotations;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
19
40
|
return (
|
|
20
41
|
<RegionList
|
|
21
42
|
activeId={activeAnnotationId}
|
|
22
|
-
annotations={
|
|
43
|
+
annotations={annotationsToShow}
|
|
23
44
|
className="ba-RegionAnnotations-list"
|
|
24
45
|
onSelect={handleAnnotationActive}
|
|
25
46
|
/>
|
|
@@ -4,6 +4,8 @@ import RegionRect, { RegionRectRef } from './RegionRect';
|
|
|
4
4
|
import { Rect } from '../@types';
|
|
5
5
|
import { CreatorItemRegion, CreatorStatus } from '../store/creator';
|
|
6
6
|
import './RegionCreation.scss';
|
|
7
|
+
import { TARGET_TYPE } from '../constants';
|
|
8
|
+
import { getVideoCurrentTimeInMilliseconds } from '../utils/useVideoTiming';
|
|
7
9
|
|
|
8
10
|
type Props = {
|
|
9
11
|
isCreating: boolean;
|
|
@@ -14,6 +16,8 @@ type Props = {
|
|
|
14
16
|
setStaged: (staged: CreatorItemRegion | null) => void;
|
|
15
17
|
setStatus: (status: CreatorStatus) => void;
|
|
16
18
|
staged?: CreatorItemRegion | null;
|
|
19
|
+
targetType: TARGET_TYPE;
|
|
20
|
+
referenceEl?: HTMLElement;
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
type State = {
|
|
@@ -45,8 +49,10 @@ export default class RegionCreation extends React.PureComponent<Props, State> {
|
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
handleStop = (shape: Rect): void => {
|
|
48
|
-
const { location, setStaged, setStatus } = this.props;
|
|
49
|
-
|
|
52
|
+
const { location, setStaged, setStatus, targetType, referenceEl } = this.props;
|
|
53
|
+
const annotationLocation = targetType !== TARGET_TYPE.FRAME ? location : getVideoCurrentTimeInMilliseconds(referenceEl as HTMLVideoElement);
|
|
54
|
+
|
|
55
|
+
setStaged({ location: annotationLocation, shape });
|
|
50
56
|
setStatus(CreatorStatus.staged);
|
|
51
57
|
};
|
|
52
58
|
|
|
@@ -14,6 +14,6 @@ export default class RegionManager extends BaseManager {
|
|
|
14
14
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
this.root.render(<RegionCreationContainer location={this.location} {...props} />);
|
|
17
|
+
this.root.render(<RegionCreationContainer targetType={this.targetType} referenceEl={this.referenceEl} location={this.location} {...props} />);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -14,6 +14,6 @@ export default class RegionListManager extends BaseManager {
|
|
|
14
14
|
this.root = ReactDOM.createRoot(this.reactEl);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
this.root.render(<RegionAnnotationsContainer location={this.location} {...props} />);
|
|
17
|
+
this.root.render(<RegionAnnotationsContainer location={this.location} referenceEl={this.referenceEl} targetType={this.targetType} {...props} />);
|
|
18
18
|
}
|
|
19
19
|
}
|
package/src/region/actions.ts
CHANGED
|
@@ -2,15 +2,17 @@ import { AppThunkDispatch, AppState } from '../store';
|
|
|
2
2
|
import { createAnnotationAction } from '../store/annotations';
|
|
3
3
|
import { getFileVersionId } from '../store/options';
|
|
4
4
|
import { Rect } from '../@types';
|
|
5
|
+
import { TARGET_TYPE } from '../constants';
|
|
5
6
|
|
|
6
7
|
export type CreateArg = {
|
|
7
8
|
location: number;
|
|
8
9
|
message: string;
|
|
9
10
|
shape: Rect;
|
|
11
|
+
targetType: TARGET_TYPE;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export const createRegionAction = (arg: CreateArg) => (dispatch: AppThunkDispatch, getState: () => AppState) => {
|
|
13
|
-
const { location, message, shape } = arg;
|
|
15
|
+
const { location, message, shape, targetType } = arg;
|
|
14
16
|
const state = getState();
|
|
15
17
|
const newAnnotation = {
|
|
16
18
|
description: {
|
|
@@ -22,7 +24,7 @@ export const createRegionAction = (arg: CreateArg) => (dispatch: AppThunkDispatc
|
|
|
22
24
|
},
|
|
23
25
|
target: {
|
|
24
26
|
location: {
|
|
25
|
-
type:
|
|
27
|
+
type: targetType,
|
|
26
28
|
value: location,
|
|
27
29
|
},
|
|
28
30
|
shape,
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import getProp from 'lodash/get';
|
|
2
2
|
import { Annotation } from '../../@types';
|
|
3
3
|
import { AppState } from '../types';
|
|
4
|
+
import { MEDIA_LOCATION_INDEX } from '../../constants';
|
|
4
5
|
|
|
5
6
|
type State = Pick<AppState, 'annotations'>;
|
|
6
7
|
|
|
7
8
|
export const getActiveAnnotationId = ({ annotations }: State): string | null => annotations.activeId;
|
|
8
9
|
export const getAnnotation = ({ annotations }: State, id: string): Annotation | undefined => annotations.byId[id];
|
|
9
10
|
export const getAnnotations = ({ annotations }: State): Annotation[] => [...Object.values(annotations.byId)];
|
|
10
|
-
export const getAnnotationsForLocation = (state: State, location: number): Annotation[] =>
|
|
11
|
-
getAnnotations(state)
|
|
11
|
+
export const getAnnotationsForLocation = (state: State, location: number): Annotation[] => {
|
|
12
|
+
const annotations = getAnnotations(state);
|
|
13
|
+
// For video annotations the location will be -1 by default as there is no page number for videos. We will return all of the annotations
|
|
14
|
+
// and the component will filter them by the current play time of the video.
|
|
15
|
+
if (location === MEDIA_LOCATION_INDEX) {
|
|
16
|
+
return annotations;
|
|
17
|
+
}
|
|
18
|
+
return annotations.filter(annotation => getProp(annotation, 'target.location.value') === location);
|
|
19
|
+
};
|
|
12
20
|
export const getIsInitialized = ({ annotations }: State): boolean => annotations.isInitialized;
|