@playkit-js/manual-hotspots 3.3.1-canary.0-7e9f2da → 3.3.1-canary.0-2996ab6

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": "@playkit-js/manual-hotspots",
3
- "version": "3.3.1-canary.0-7e9f2da",
3
+ "version": "3.3.1-canary.0-2996ab6",
4
4
  "private": false,
5
5
  "bugs": {
6
6
  "url": "https://github.com/kaltura-ps/playkit-js-manual-hotspots/issues"
@@ -42,8 +42,7 @@
42
42
  "docs",
43
43
  "LICENSE",
44
44
  "README.md",
45
- "CHANGELOG.md",
46
- "src"
45
+ "CHANGELOG.md"
47
46
  ],
48
47
  "scripts": {
49
48
  "disabled-preinstall": "npx --yes --prefer-online --registry=https://npm.pkg.github.com --package @kaltura/kaltura-tools@latest kaltura-tools check --root=.",
@@ -1,267 +0,0 @@
1
- import {h, Component} from 'preact';
2
- import {A11yWrapper} from '@playkit-js/common';
3
- import {LayoutHotspot} from '../utils/hotspot';
4
- import {AnalyticsEvents} from '../utils/analyticsEvents';
5
- import {HotspotsEvents} from '../events/events';
6
-
7
- const defaultContainerStyles = {
8
- position: 'absolute',
9
- display: 'table',
10
- boxSizing: 'border-box'
11
- };
12
-
13
- const defaultButtonsStyles = {
14
- position: 'relative',
15
- width: '100%',
16
- height: 'inherit',
17
- appearance: 'none',
18
- border: 'none',
19
- display: 'flex',
20
- cursor: 'pointer',
21
- wordBreak: 'break-all',
22
- textRendering: 'geometricPrecision',
23
- overflow: 'hidden',
24
- textOverflow: 'ellipsis',
25
- padding: '3px',
26
- alignItems: 'center',
27
- justifyContent: 'center',
28
- outline: 0,
29
- lineHeight: 'normal'
30
- };
31
-
32
- type Props = {
33
- visible: boolean;
34
- hotspot: LayoutHotspot;
35
- styles?: {[key: string]: any};
36
- pauseVideo(): void;
37
- exitFullscreen(): void;
38
- seekTo(time: number): void;
39
- sendAnalytics(event: AnalyticsEvents): void;
40
- dispatcher(name: string, payload?: any): void
41
- };
42
-
43
- type State = {
44
- disableClick: boolean;
45
- isReady: boolean;
46
- };
47
-
48
- const defaultProps = {
49
- styles: {}
50
- };
51
-
52
- function prepareUrl(url: string): string {
53
- if (url.startsWith('mailto:')) {
54
- return url;
55
- }
56
- if (!url.match(/^https?:\/\//i)) {
57
- url = 'http://' + url;
58
- }
59
- return url;
60
- }
61
-
62
- function scrollToAnchor(selector: string, scrollBehavior: ScrollBehavior = 'smooth', offsetY: number = 0): void {
63
- const element = document.querySelector(selector);
64
-
65
- if (!element) {
66
- return;
67
- }
68
-
69
- if (offsetY !== 0) {
70
- const elementRect = element.getBoundingClientRect();
71
- const absoluteTop = elementRect.top + window.scrollY - offsetY;
72
- window.scrollTo({
73
- top: absoluteTop,
74
- behavior: scrollBehavior
75
- });
76
- } else {
77
- element.scrollIntoView({behavior: scrollBehavior});
78
- }
79
- }
80
-
81
- function sendPostMessage(message: unknown, targetOrigin: string = '*', hotspotId: string, hotspotLabel?: string): void {
82
- const payload = {
83
- source: 'kaltura-hotspot',
84
- hotspotId,
85
- hotspotLabel,
86
- data: message,
87
- timestamp: Date.now()
88
- };
89
-
90
- try {
91
- window.top?.postMessage(payload, targetOrigin);
92
- } catch (e) {
93
- // do nothing
94
- }
95
- }
96
-
97
- const MINIMAL_FONT_SIZE = 10;
98
-
99
- export default class Hotspot extends Component<Props, State> {
100
- static defaultProps = defaultProps;
101
-
102
- hotspotRef: HTMLDivElement | null = null;
103
-
104
- state = {
105
- disableClick: true,
106
- isReady: false
107
- };
108
-
109
- componentDidMount() {
110
- const {hotspot, dispatcher} = this.props;
111
-
112
- if (!hotspot || !hotspot.onClick) {
113
- this.setState({ isReady: true });
114
- return;
115
- }
116
-
117
- this.setState({
118
- isReady: true,
119
- disableClick: !this.isClickable()
120
- });
121
-
122
- const {id, label} = hotspot;
123
- dispatcher(HotspotsEvents.HOTSPOT_DISPLAYED, {id, label});
124
- }
125
-
126
- isClickable = (): boolean => {
127
- const {
128
- hotspot: {onClick}
129
- } = this.props;
130
-
131
- if (!onClick) {
132
- return false;
133
- }
134
-
135
- switch (onClick.type) {
136
- case 'jumpToTime':
137
- return typeof onClick.jumpToTime !== 'undefined';
138
- case 'openUrl':
139
- case 'openUrlInNewTab':
140
- return !!onClick.url;
141
- case 'scrollToAnchor':
142
- return !!onClick.selector;
143
- case 'postMessage':
144
- return onClick.message !== undefined;
145
- default:
146
- return false;
147
- }
148
- };
149
-
150
- handleClick = () => {
151
- const {hotspot, dispatcher} = this.props;
152
- const {disableClick} = this.state;
153
-
154
- const {id, label} = hotspot;
155
- dispatcher(HotspotsEvents.HOTSPOT_CLICK, {id, label});
156
-
157
- if (!hotspot.onClick || disableClick) {
158
- return;
159
- }
160
-
161
- switch (hotspot.onClick.type) {
162
- case 'jumpToTime':
163
- if (typeof hotspot.onClick.jumpToTime === 'undefined') {
164
- return;
165
- }
166
-
167
- this.props.seekTo(hotspot.onClick.jumpToTime / 1000);
168
- break;
169
- case 'openUrl':
170
- {
171
- if (!hotspot.onClick.url) {
172
- return;
173
- }
174
- const url = prepareUrl(hotspot.onClick.url);
175
- window.open(url, '_top');
176
- this.props.sendAnalytics({
177
- eventNumber: 47,
178
- target: url,
179
- hotspotId: hotspot.id
180
- });
181
- }
182
- break;
183
- case 'openUrlInNewTab':
184
- {
185
- if (!hotspot.onClick.url) {
186
- return;
187
- }
188
-
189
- this.props.pauseVideo();
190
-
191
- const url = prepareUrl(hotspot.onClick.url);
192
- try {
193
- window.open(url, '_blank');
194
- this.props.sendAnalytics({
195
- eventNumber: 47,
196
- target: url,
197
- hotspotId: hotspot.id
198
- });
199
- } catch (e) {
200
- // do nothing
201
- }
202
- }
203
- break;
204
-
205
- case 'scrollToAnchor':
206
- {
207
- if (!hotspot.onClick.selector) {
208
- return;
209
- }
210
- this.props.exitFullscreen();
211
- this.props.pauseVideo();
212
- scrollToAnchor(hotspot.onClick.selector, hotspot.onClick.scrollBehavior, hotspot.onClick.offsetY);
213
- }
214
- break;
215
-
216
- case 'postMessage':
217
- {
218
- if (hotspot.onClick.message === undefined) {
219
- return;
220
- }
221
- if (hotspot.onClick.pauseVideo) {
222
- this.props.pauseVideo();
223
- }
224
- sendPostMessage(hotspot.onClick.message, hotspot.onClick.targetOrigin, id, label);
225
- }
226
- break;
227
-
228
- default:
229
- break;
230
- }
231
- };
232
-
233
- render() {
234
- const {hotspot} = this.props;
235
- const {layout, label} = hotspot;
236
- const {isReady, disableClick} = this.state;
237
-
238
- if (!isReady || !this.props.visible) {
239
- return null;
240
- }
241
-
242
- const containerStyles = {
243
- ...defaultContainerStyles,
244
- top: layout.y,
245
- left: layout.x,
246
- height: layout.height,
247
- width: layout.width
248
- };
249
-
250
- const buttonStyles = {
251
- ...defaultButtonsStyles,
252
- ...hotspot.styles,
253
- cursor: disableClick ? 'default' : 'pointer',
254
- maxWidth: `${layout.width}px`,
255
- fontSize: `${hotspot.relativeStyle.fontSize}px`,
256
- borderRadius: `${hotspot.relativeStyle.radiusBorder}px`,
257
- };
258
-
259
- return (
260
- <A11yWrapper onClick={this.handleClick} role='alert' aria-live="polite">
261
- <div ref={(ref) => (this.hotspotRef = ref)} tabIndex={0} role="button" aria-label={label} aria-disabled={disableClick} style={containerStyles} data-testid="hotspots_hotspot">
262
- <div style={buttonStyles}>{label}</div>
263
- </div>
264
- </A11yWrapper>
265
- );
266
- }
267
- }
@@ -1,115 +0,0 @@
1
- import {h, Component, createRef} from 'preact';
2
- import {LayoutHotspot, shallowCompareHotspots} from '../utils/hotspot';
3
- import Hotspot from './Hotspot';
4
- import {AnalyticsEvents} from '../utils/analyticsEvents';
5
-
6
- const { withPlayer } = KalturaPlayer.ui.components;
7
- const { withText, Text } = KalturaPlayer.ui.preacti18n;
8
- const translates = {
9
- hotspotRemoved: (
10
- <Text id="hotspots.hotspot_removed" fields={{ hotspotLabel: '' }}>
11
- {`{hotspotLabel} hotspot removed`}
12
- </Text>
13
- )
14
- };
15
-
16
- const hotspotsContainerStyles = {
17
- position: 'absolute',
18
- display: 'block',
19
- overflow: 'visible',
20
- top: 0,
21
- left: 0,
22
- width: 0,
23
- height: 0
24
- };
25
-
26
- export interface Props {
27
- hotspots: LayoutHotspot[];
28
- pauseVideo(): void;
29
- seekTo(time: number): void;
30
- exitFullscreen(): void;
31
- sendAnalytics(event: AnalyticsEvents): void;
32
- dispatcher(name: string, payload?: any): void;
33
- hotspotRemoved?: string;
34
- player?: any;
35
- }
36
-
37
- @withPlayer
38
- @withText(translates)
39
- export default class HotspotWrapper extends Component<Props> {
40
-
41
- private liveRegionRef = createRef<HTMLDivElement>();
42
- private previousHotspotMap: Map<string, string> = new Map();
43
-
44
- shouldComponentUpdate(nextProps: Props) {
45
- return !shallowCompareHotspots(this.props.hotspots, nextProps.hotspots);
46
- }
47
- componentDidUpdate() {
48
- const currentMap = new Map<string, string>(
49
- this.props.hotspots
50
- .filter(h => typeof h.label === 'string')
51
- .map(h => [h.id, h.label!])
52
- );
53
-
54
- let announced = false;
55
- this.previousHotspotMap.forEach((label, id) => {
56
- if (!currentMap.has(id) && !announced) {
57
- if (this.props.hotspotRemoved) {
58
- const message = this.props.hotspotRemoved.replace('{hotspotLabel}', label);
59
- this.announceHotspotChange(message);
60
- }
61
- announced = true;
62
- }
63
- });
64
-
65
- this.previousHotspotMap = currentMap;
66
- }
67
-
68
- private announceHotspotChange(message: string) {
69
- const liveRegion = this.liveRegionRef.current;
70
- if (!liveRegion) return;
71
- liveRegion.textContent = '';
72
- //setTimeout ensures the DOM change happens in two separate cycles
73
- // which solves the issue that the hotspot removal is announced only the first time.
74
- setTimeout(() => {
75
- liveRegion.textContent = message;
76
- });
77
- }
78
-
79
- private renderHotspots = (visualHotspot: LayoutHotspot[]) => {
80
- if (!visualHotspot) {
81
- return null;
82
- }
83
-
84
- const {seekTo, pauseVideo, exitFullscreen, sendAnalytics, dispatcher} = this.props;
85
- return visualHotspot.map(hotspotData => (
86
- <Hotspot dispatcher={dispatcher} pauseVideo={pauseVideo} exitFullscreen={exitFullscreen} seekTo={seekTo} key={hotspotData.id} visible={true} hotspot={hotspotData} sendAnalytics={sendAnalytics} />
87
- ));
88
- };
89
-
90
- render() {
91
- const {hotspots} = this.props;
92
- const hotspotsElements = this.renderHotspots(hotspots);
93
- const targetId = this.props.player?.config?.targetId;
94
- const liveRegionId = `hotspot-liveRegion-${targetId}`;
95
-
96
- return (
97
- <div style={hotspotsContainerStyles} data-testid="hotspots_hotspotsContainer">
98
- {hotspotsElements}
99
- <div
100
- ref={this.liveRegionRef}
101
- id={liveRegionId}
102
- aria-live="polite"
103
- role="status"
104
- style={{
105
- position: 'absolute',
106
- left: '-9999px',
107
- width: '1px',
108
- height: '1px',
109
- overflow: 'hidden'
110
- }}
111
- />
112
- </div>
113
- );
114
- }
115
- }
@@ -1,4 +0,0 @@
1
- export const HotspotsEvents = {
2
- HOTSPOT_CLICK: 'manual_hotspot_click',
3
- HOTSPOT_DISPLAYED: 'manual_hotspot_displayed',
4
- }
package/src/global.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare module '*.scss' {
2
- const content: {[className: string]: string};
3
- export = content;
4
- }
@@ -1,180 +0,0 @@
1
- import {h, ComponentChildren} from 'preact';
2
- import {FloatingItem, FloatingManager, FloatingItemProps} from '@playkit-js/ui-managers';
3
-
4
- import {RawLayoutHotspot, LayoutHotspot, Canvas, RawFloatingCuepoint, Layout, Style} from './utils/hotspot';
5
- import HotspotWrapper from './components/HotspotWrapper';
6
- import {ScaleCalculation, scaleVideo} from './utils/scale-video';
7
- import {HotspotTimelineSimple} from './utils/hotspot-timeline-simple';
8
- import {HotspotInputConfig} from './utils/hotspot-config';
9
- import {HotspotLoader} from './utils/hotspot-loader';
10
-
11
- interface HotspotsPluginConfig {
12
- hotspots?: HotspotInputConfig[];
13
- }
14
-
15
- export class HotspotsPlugin extends KalturaPlayer.core.BasePlugin {
16
- static defaultConfig: HotspotsPluginConfig = {
17
- hotspots: []
18
- };
19
-
20
- private _player: KalturaPlayerTypes.Player;
21
- private _hotspots: LayoutHotspot[] = [];
22
- private _floatingItem: FloatingItem | null = null;
23
- private _canvas: Canvas | null = null;
24
- private _timeline: HotspotTimelineSimple<RawLayoutHotspot> = new HotspotTimelineSimple();
25
-
26
- constructor(name: string, player: KalturaPlayerTypes.Player, config: HotspotsPluginConfig) {
27
- super(name, player, config);
28
- this._player = player;
29
- }
30
-
31
- static isValid(): boolean {
32
- return true;
33
- }
34
-
35
- private get floatingManager(): FloatingManager {
36
- return (this.player.getService('floatingManager') as FloatingManager) || {};
37
- }
38
-
39
- loadMedia(): void {
40
- this._addHotspotsContainer();
41
- this._initTimeline();
42
- this.eventManager.listen(this._player, this._player.Event.TIME_UPDATE, (): void => {
43
- const currentTimeMs = this._player.currentTime * 1000;
44
-
45
- if (this._timeline.update(currentTimeMs)) {
46
- this._hotspots = this._recalculateCuepointLayout(this._timeline.getVisibleHotspots());
47
- this._updateHotspotsContainer();
48
- }
49
- });
50
- }
51
-
52
- private _initTimeline(): void {
53
- const loader = new HotspotLoader(this.logger);
54
- const rawHotspots = loader.load(this.config.hotspots);
55
-
56
- if (!rawHotspots) {
57
- return;
58
- }
59
-
60
- this._timeline.initialize(rawHotspots);
61
- }
62
-
63
- private _calculateLayout(cuepoint: RawFloatingCuepoint, scaleCalculation: ScaleCalculation): Layout {
64
- const {rawLayout} = cuepoint;
65
- return {
66
- x: scaleCalculation.left + rawLayout.relativeX * scaleCalculation.width,
67
- y: scaleCalculation.top + rawLayout.relativeY * scaleCalculation.height,
68
- width: rawLayout.relativeWidth * scaleCalculation.width,
69
- height: rawLayout.relativeHeight * scaleCalculation.height
70
- };
71
- }
72
-
73
- private _calculateStyle(cuepoint: LayoutHotspot, scaleCalculation: ScaleCalculation): Style {
74
- const {rawLayout, styles} = cuepoint;
75
- return {
76
- fontSize: parseInt(styles['font-size'])/ rawLayout.stageWidth * scaleCalculation.width,
77
- radiusBorder: parseInt(styles['border-radius'])/ rawLayout.stageWidth * scaleCalculation.width,
78
- };
79
- }
80
-
81
- private _recalculateCuepointLayout = (hotspots: RawLayoutHotspot[] | LayoutHotspot[]): LayoutHotspot[] => {
82
- this.logger.debug('calculating cuepoint layout based on video/player sizes');
83
-
84
- if (!this._canvas?.playerSize || !this._canvas?.videoSize) {
85
- this.logger.warn('missing video/player sizes, hide all cuepoint');
86
- return [];
87
- }
88
-
89
- const {width: playerWidth, height: playerHeight} = this._canvas.playerSize;
90
- const {width: videoWidth, height: videoHeight} = this._canvas.videoSize;
91
- const canCalcaulateLayout = playerWidth && playerHeight && videoWidth && videoHeight;
92
-
93
- if (!canCalcaulateLayout) {
94
- this.logger.warn('missing video/player sizes, hide all cuepoint');
95
- return [];
96
- }
97
-
98
- const scaleCalculation = scaleVideo(videoWidth, videoHeight, playerWidth, playerHeight, true);
99
-
100
- this.logger.debug('recalculate cuepoint layout based on new sizes');
101
- return hotspots.map(cuepoint => ({
102
- ...cuepoint,
103
- layout: this._calculateLayout(cuepoint as any, scaleCalculation),
104
- relativeStyle: this._calculateStyle(cuepoint as any, scaleCalculation)
105
- }));
106
- };
107
-
108
- private _addHotspotsContainer(): void {
109
- // TODO - fire on click
110
- this._floatingItem = this.floatingManager.add({
111
- label: 'Manual Hotspots',
112
- mode: 'MediaLoaded',
113
- position: 'VideoArea',
114
- renderContent: this._renderRoot
115
- });
116
- }
117
-
118
- private _updateHotspotsContainer(): void {
119
- if (this._floatingItem) {
120
- this._floatingItem.update();
121
- }
122
- }
123
-
124
- private _pauseVideo = (): void => {
125
- this._player.pause();
126
- };
127
- private _seekTo = (time: number): void => {
128
- this._player.currentTime = time;
129
- };
130
- private _exitFullscreen = (): void => {
131
- // @ts-expect-error bad types
132
- if (this._player.isFullscreen()) {
133
- // @ts-expect-error bad types
134
- this._player.exitFullscreen();
135
- }
136
- };
137
-
138
- private _checkIfResizeHappened = (newCanvas: Canvas): boolean => {
139
- if (!this._canvas) {
140
- this._canvas = newCanvas;
141
- return true;
142
- }
143
- const prevPlayerSize = this._canvas.playerSize;
144
- const prevVideoSize = this._canvas.videoSize;
145
- const nextPlayerSize = newCanvas.playerSize;
146
- const nextVideoSize = newCanvas.videoSize;
147
- if (
148
- prevPlayerSize.width !== nextPlayerSize.width ||
149
- prevPlayerSize.height !== nextPlayerSize.height ||
150
- prevVideoSize.width !== nextVideoSize.width ||
151
- prevVideoSize.height !== nextVideoSize.height
152
- ) {
153
- this._canvas = newCanvas;
154
- return true;
155
- }
156
- return false;
157
- };
158
-
159
- private _renderRoot = (floatingItemProps: FloatingItemProps): ComponentChildren => {
160
- if (this._checkIfResizeHappened(floatingItemProps.canvas)) {
161
- this._hotspots = this._recalculateCuepointLayout(this._hotspots);
162
- }
163
- return (
164
- <HotspotWrapper dispatcher={(eventType, payload) => this.dispatchEvent(eventType, payload)} key={'hotspotWrapper'} hotspots={this._hotspots} pauseVideo={this._pauseVideo} seekTo={this._seekTo} exitFullscreen={this._exitFullscreen} sendAnalytics={() => {}} />
165
- );
166
- };
167
-
168
- reset(): void {
169
- this.eventManager.removeAll();
170
- this._timeline.clear();
171
- this._hotspots = [];
172
- this._canvas = null;
173
- if (this._floatingItem) {
174
- this.floatingManager.remove(this._floatingItem);
175
- this._floatingItem = null;
176
- }
177
- }
178
-
179
- destroy(): void {}
180
- }
package/src/index.ts DELETED
@@ -1,15 +0,0 @@
1
- import {HotspotsPlugin} from './hotspots-plugin';
2
- import {registerPlugin} from '@playkit-js/kaltura-player-js';
3
-
4
- declare var __VERSION__: string;
5
- declare var __NAME__: string;
6
-
7
- const VERSION = __VERSION__;
8
- const NAME = __NAME__;
9
-
10
- export {HotspotsPlugin as Plugin};
11
- export {VERSION, NAME};
12
- export {HotspotsEvents} from './events/events';
13
-
14
- const pluginName: string = 'playkit-js-manual-hotspots';
15
- registerPlugin(pluginName, HotspotsPlugin as any);
@@ -1,3 +0,0 @@
1
- export type HotspotClick = { eventNumber: 47; hotspotId: string; target: string };
2
-
3
- export type AnalyticsEvents = HotspotClick;
@@ -1,54 +0,0 @@
1
- import {PositionPreset, SizePreset} from './hotspot-presets';
2
- import {OnClickAction} from './hotspot';
3
-
4
- interface StyleConfig {
5
- background?: string;
6
- color?: string;
7
- 'border-radius'?: string;
8
- 'font-size'?: string;
9
- 'font-weight'?: string;
10
- 'font-family'?: string;
11
- [key: string]: string | undefined;
12
- }
13
-
14
- export interface HotspotInputConfig {
15
- id?: string;
16
- startTime: number;
17
- endTime?: number;
18
- label?: string;
19
- layout?: {
20
- position?: PositionPreset;
21
- size?: SizePreset;
22
-
23
- // overrides position preset
24
- relativeX?: number;
25
- relativeY?: number;
26
-
27
- // overrides size preset
28
- relativeWidth?: number;
29
- relativeHeight?: number;
30
-
31
- // overrides for stage size used in font scaling
32
- stageWidth?: number;
33
- stageHeight?: number;
34
- };
35
- styles?: StyleConfig;
36
- onClick?: OnClickAction;
37
- }
38
-
39
- export interface HotspotConfigFull {
40
- id: string;
41
- startTime: number;
42
- endTime?: number;
43
- label?: string;
44
- layout: {
45
- relativeX: number;
46
- relativeY: number;
47
- relativeWidth: number;
48
- relativeHeight: number;
49
- stageWidth: number;
50
- stageHeight: number;
51
- };
52
- styles: StyleConfig;
53
- onClick?: OnClickAction;
54
- }