box-annotations 5.1.0 → 5.2.1-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "box-annotations",
3
- "version": "5.1.0",
3
+ "version": "5.2.1-beta.0",
4
4
  "description": "Box Annotations",
5
5
  "author": "Box (https://www.box.com/)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -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: 'page';
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: Page;
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: Page;
117
+ location: Location;
110
118
  type: 'point';
111
119
  }
112
120
 
113
121
  export interface TargetRegion {
114
- location: Page;
122
+ location: Location;
115
123
  shape: Rect;
116
124
  type: 'region';
117
125
  }
@@ -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?: 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
- constructor({ location = 1, referenceEl, resinTags = {} }: Options) {
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();
@@ -27,6 +27,7 @@
27
27
  }
28
28
  }
29
29
 
30
+ // Legacy styles (default)
30
31
  .ba-PopupDrawingToolbar {
31
32
  background-color: $bdl-gray;
32
33
  border-radius: $bdl-border-radius-size;
@@ -44,6 +45,19 @@
44
45
  }
45
46
  }
46
47
 
48
+ // Modernized styles (when feature flag is enabled)
49
+ .ba-PopupDrawingToolbar--modernized {
50
+ background-color: transparent;
51
+ border-radius: 46px;
52
+
53
+ .ba-Popup-content {
54
+ gap: 4px;
55
+ padding: 8px;
56
+ background-color: rgba(0, 0, 0, .8);
57
+ border-radius: 46px;
58
+ }
59
+ }
60
+
47
61
  .ba-PopupDrawingToolbar-group {
48
62
  position: relative;
49
63
  display: flex;
@@ -53,6 +67,15 @@
53
67
  & + & {
54
68
  border-left: 1px solid #000;
55
69
  }
70
+
71
+ .ba-PopupDrawingToolbar--modernized & {
72
+ gap: 4px;
73
+ }
74
+ }
75
+
76
+ // Modernized border-left for adjacent groups
77
+ .ba-PopupDrawingToolbar--modernized .ba-PopupDrawingToolbar-group + .ba-PopupDrawingToolbar-group {
78
+ border-left: 1px solid rgb(255, 255, 255, 8%);
56
79
  }
57
80
 
58
81
  .ba-PopupDrawingToolbar-delete,
@@ -65,6 +88,7 @@
65
88
  }
66
89
  }
67
90
 
91
+ // Legacy comment button styles
68
92
  .ba-PopupDrawingToolbar-comment {
69
93
  @include ba-PopupDrawingToolbarButton($width: max-content);
70
94
  @include common-typography;
@@ -74,3 +98,10 @@
74
98
  color: $white;
75
99
  font-weight: bold;
76
100
  }
101
+
102
+ // Modernized comment button styles
103
+ .ba-PopupDrawingToolbar--modernized .ba-PopupDrawingToolbar-comment {
104
+ padding-right: 8px;
105
+ padding-left: 12px;
106
+ font-size: 15px;
107
+ }
@@ -1,13 +1,21 @@
1
1
  import React from 'react';
2
+ import { useSelector } from 'react-redux';
2
3
  import classNames from 'classnames';
3
4
  import { FormattedMessage, useIntl } from 'react-intl';
5
+ // Legacy icons (box-ui-elements)
4
6
  import IconRedo from 'box-ui-elements/es/icon/line/Redo16';
5
7
  import IconTrash from 'box-ui-elements/es/icon/line/Trash16';
6
8
  import IconUndo from 'box-ui-elements/es/icon/line/Undo16';
9
+ // Modernized icons
10
+ import ArrowBack from '../../icons/ArrowBack';
11
+ import ArrowForward from '../../icons/ArrowForward';
12
+ import Trash from '../../icons/Trash';
7
13
  import messages from './messages';
8
14
  import PopupBase from './PopupBase';
9
15
  import { Options, PopupReference, Rect } from './Popper';
10
16
  import './PopupDrawingToolbar.scss';
17
+ import { isFeatureEnabled } from '../../store/options/selectors';
18
+ import { AppState } from '../../store/types';
11
19
 
12
20
  export type PopupBaseRef = PopupBase;
13
21
 
@@ -48,14 +56,20 @@ const options: Partial<Options> = {
48
56
  placement: 'top',
49
57
  };
50
58
 
59
+ const ICON_SIZE = 20;
60
+
51
61
  const PopupDrawingToolbar = (props: Props, ref: React.Ref<PopupBaseRef>): JSX.Element => {
52
62
  const { canComment, canRedo, canUndo, className, onDelete, onRedo, onReply, onUndo, reference } = props;
63
+ const isModernizationEnabled = useSelector((state: AppState) => isFeatureEnabled(state, 'modernization'));
64
+
53
65
  const intl = useIntl();
54
66
 
55
67
  return (
56
68
  <PopupBase
57
69
  ref={ref}
58
- className={classNames(className, 'ba-PopupDrawingToolbar')}
70
+ className={classNames(className, 'ba-PopupDrawingToolbar', {
71
+ 'ba-PopupDrawingToolbar--modernized': isModernizationEnabled,
72
+ })}
59
73
  data-resin-component="popupDrawingToolbar"
60
74
  options={options}
61
75
  reference={reference}
@@ -69,7 +83,7 @@ const PopupDrawingToolbar = (props: Props, ref: React.Ref<PopupBaseRef>): JSX.El
69
83
  title={intl.formatMessage(messages.drawingButtonUndo)}
70
84
  type="button"
71
85
  >
72
- <IconUndo />
86
+ {isModernizationEnabled ? <ArrowBack height={ICON_SIZE} width={ICON_SIZE} /> : <IconUndo />}
73
87
  </button>
74
88
  <button
75
89
  className="ba-PopupDrawingToolbar-redo"
@@ -79,7 +93,7 @@ const PopupDrawingToolbar = (props: Props, ref: React.Ref<PopupBaseRef>): JSX.El
79
93
  title={intl.formatMessage(messages.drawingButtonRedo)}
80
94
  type="button"
81
95
  >
82
- <IconRedo />
96
+ {isModernizationEnabled ? <ArrowForward height={ICON_SIZE} width={ICON_SIZE} /> : <IconRedo />}
83
97
  </button>
84
98
  <button
85
99
  className="ba-PopupDrawingToolbar-delete"
@@ -88,7 +102,7 @@ const PopupDrawingToolbar = (props: Props, ref: React.Ref<PopupBaseRef>): JSX.El
88
102
  title={intl.formatMessage(messages.drawingButtonDelete)}
89
103
  type="button"
90
104
  >
91
- <IconTrash />
105
+ {isModernizationEnabled ? <Trash height={ICON_SIZE} width={ICON_SIZE} /> : <IconTrash />}
92
106
  </button>
93
107
  </div>
94
108
  <div className="ba-PopupDrawingToolbar-group">
@@ -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
- setStaged({ location, pathGroups: drawnPathGroups });
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={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
  }
@@ -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: 'page' as const,
28
+ type: targetType,
27
29
  value: location,
28
30
  },
29
31
  path_groups: pathGroups,
@@ -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: 'page' as const,
27
+ type: TARGET_TYPE.PAGE as const,
27
28
  value: location,
28
29
  },
29
30
  shapes,
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+
3
+ function ArrowBack(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ArrowBack"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 20 20"
12
+ width="1em"
13
+ {...props}
14
+ >
15
+ <path
16
+ d="M6.91086 1.91058C7.2363 1.58515 7.76381 1.58515 8.08925 1.91058C8.41462 2.23602 8.41462 2.76355 8.08925 3.08896L5.34511 5.8331H12.5C14.1062 5.83312 15.3897 6.4396 16.2574 7.47291C17.1089 8.4871 17.5 9.83869 17.5 11.2498C17.5 12.6609 17.109 14.0125 16.2574 15.0266C15.3897 16.0599 14.1062 16.6664 12.5 16.6664H10C9.53987 16.6664 9.16679 16.2933 9.16671 15.8331C9.16671 15.3729 9.53979 14.9998 10 14.9998H12.5C13.6552 14.9998 14.4553 14.5803 14.9805 13.9549C15.5219 13.3102 15.8334 12.3699 15.8334 11.2498C15.8334 10.1296 15.5219 9.18935 14.9805 8.54469C14.4553 7.91928 13.6552 7.49979 12.5 7.49977H5.34511L8.08925 10.2439C8.41462 10.5694 8.41471 11.0969 8.08925 11.4223C7.76384 11.7477 7.23631 11.7477 6.91086 11.4223L2.7442 7.25563C2.41876 6.9302 2.41876 6.40268 2.7442 6.07725L6.91086 1.91058Z"
17
+ fill="#fff"
18
+ />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ export default ArrowBack;
@@ -0,0 +1,23 @@
1
+ import * as React from 'react';
2
+
3
+ function ArrowForward(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="ArrowForward"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 20 20"
12
+ width="1em"
13
+ {...props}
14
+ >
15
+ <path
16
+ d="M11.9108 1.91058C12.2363 1.58515 12.7638 1.58515 13.0892 1.91058L17.2558 6.07725C17.5813 6.40268 17.5813 6.9302 17.2558 7.25563L13.0892 11.4223C12.7638 11.7477 12.2363 11.7477 11.9108 11.4223C11.5854 11.0969 11.5854 10.5694 11.9108 10.2439L14.6549 7.49977H7.5C6.34482 7.49977 5.54479 7.91927 5.01953 8.54469C4.4782 9.18935 4.16667 10.1296 4.16667 11.2498C4.16668 12.3699 4.47819 13.3102 5.01953 13.9549C5.54479 14.5803 6.34484 14.9998 7.5 14.9998H10C10.4603 14.9998 10.8333 15.3729 10.8333 15.8331C10.8333 16.2933 10.4603 16.6664 10 16.6664H7.5C5.89388 16.6664 4.6104 16.0599 3.74268 15.0266C2.89112 14.0125 2.50001 12.6609 2.5 11.2498C2.5 9.83869 2.89113 8.4871 3.74268 7.47291C4.6104 6.43958 5.89385 5.8331 7.5 5.8331H14.6549L11.9108 3.08896C11.5854 2.76354 11.5854 2.23602 11.9108 1.91058Z"
17
+ fill="#fff"
18
+ />
19
+ </svg>
20
+ );
21
+ }
22
+
23
+ export default ArrowForward;
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+
3
+ function Trash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+ return (
5
+ <svg
6
+ data-testid="Trash"
7
+ fill="none"
8
+ focusable="false"
9
+ height="1em"
10
+ role="img"
11
+ viewBox="0 0 20 20"
12
+ width="1em"
13
+ {...props}
14
+ >
15
+ <path
16
+ clipRule="evenodd"
17
+ d="M7.55758 4.25017C7.6296 3.87481 7.75758 3.73256 7.87369 3.65128C7.99454 3.56669 8.19891 3.47991 8.56146 3.41949C8.92362 3.35914 9.39079 3.33317 9.99996 3.33317C10.6092 3.33317 11.0764 3.35914 11.4385 3.41949C11.801 3.47991 12.0054 3.56669 12.1263 3.65128C12.2424 3.73256 12.3704 3.87481 12.4424 4.25017C11.7196 4.1984 10.9065 4.1665 9.99996 4.1665C9.09346 4.1665 8.2803 4.1984 7.55758 4.25017ZM5.85575 4.42505C5.92957 3.5488 6.20244 2.78673 6.91792 2.2859C7.31791 2.0059 7.79063 1.8583 8.28746 1.7755C8.78471 1.69262 9.35921 1.6665 9.99996 1.6665C10.6408 1.6665 11.2153 1.69262 11.7125 1.7755C12.2093 1.8583 12.682 2.0059 13.082 2.2859C13.7975 2.78673 14.0704 3.5488 14.1442 4.42505C14.5419 4.47947 14.8961 4.53848 15.208 4.59789C15.7812 4.70707 16.2109 4.81752 16.5036 4.90318C16.6499 4.946 16.762 4.98262 16.8405 5.00976C16.8797 5.02333 16.9105 5.03452 16.9331 5.04294L16.9608 5.05343L16.97 5.05701L16.9735 5.05837L16.9749 5.05895C16.9752 5.05906 16.9761 5.05944 16.6666 5.83317L16.9761 5.05944C17.4035 5.23037 17.6113 5.71535 17.4404 6.14266C17.2975 6.49966 16.9356 6.70348 16.5711 6.66122L16.0734 15.1939C15.9706 16.9565 14.5113 18.3332 12.7457 18.3332H7.25422C5.48867 18.3332 4.02937 16.9565 3.92655 15.1939L3.42881 6.66122C3.06437 6.70348 2.70237 6.49966 2.55958 6.14266C2.38865 5.71535 2.5965 5.23037 3.02382 5.05944L3.33331 5.83317C3.02382 5.05944 3.02351 5.05955 3.02382 5.05944L3.02506 5.05895L3.02649 5.05837L3.02994 5.05701L3.03914 5.05343L3.06682 5.04294C3.0894 5.03452 3.12027 5.02333 3.15952 5.00976C3.23802 4.98262 3.35004 4.946 3.49637 4.90318C3.78904 4.81752 4.21877 4.70707 4.79197 4.59789C5.10387 4.53848 5.45812 4.47947 5.85575 4.42505ZM5.59039 15.0969L5.07378 6.24087L5.10382 6.23512C6.1447 6.03685 7.75911 5.83317 9.99996 5.83317C12.2409 5.83317 13.8553 6.03685 14.8961 6.23512L14.9261 6.24087L14.4095 15.0969C14.3581 15.9782 13.6285 16.6665 12.7457 16.6665H7.25422C6.37145 16.6665 5.6418 15.9782 5.59039 15.0969ZM11.6666 7.49984C12.1269 7.49984 12.5 7.87294 12.5 8.33317V14.1665C12.5 14.6268 12.1269 14.9998 11.6666 14.9998C11.2064 14.9998 10.8333 14.6268 10.8333 14.1665V8.33317C10.8333 7.87294 11.2064 7.49984 11.6666 7.49984ZM9.16662 8.33317C9.16662 7.87294 8.79354 7.49984 8.33329 7.49984C7.87307 7.49984 7.49997 7.87294 7.49997 8.33317V14.1665C7.49997 14.6268 7.87307 14.9998 8.33329 14.9998C8.79354 14.9998 9.16662 14.6268 9.16662 14.1665V8.33317Z"
18
+ fill="#fff"
19
+ fillRule="evenodd"
20
+ />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ export default Trash;
@@ -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,141 @@
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 | undefined, defaultLocation: number = -1 ): void {
124
+
125
+ if (!annotationId) {
126
+ return;
127
+ }
128
+ const annotation = getAnnotation(this.store.getState(), annotationId);
129
+ const annotationLocation = annotation?.target?.location?.value ?? defaultLocation;
130
+ const video = this.getReference();
131
+
132
+ if ( !video || !this.annotatedEl || annotationLocation === -1 ) {
133
+ return;
134
+ }
135
+
136
+ video.pause();
137
+ video.currentTime = annotationLocation / 1000;
138
+
139
+ }
140
+ }
141
+