@playkit-js/transcript 2.1.4 → 2.1.5-canary.16-1ac3e09

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +5 -5
  3. package/README.md +122 -24
  4. package/dist/1501adfdf5c835667ce7.svg +9 -0
  5. package/dist/301e7a199b2cd06c2edf.svg +9 -0
  6. package/dist/33bce27c0f546e80478c.svg +36 -0
  7. package/dist/6a4867d3d9170cc2a24d.svg +9 -0
  8. package/dist/73bab0af28a1c7aed29f.svg +9 -0
  9. package/dist/84087eb1cff72e5e6bd3.svg +9 -0
  10. package/dist/95d7192dc427afb678d0.svg +9 -0
  11. package/dist/cea5d6a7f050cbd199a1.svg +9 -0
  12. package/dist/d93f06ff32cdfcd016df.svg +9 -0
  13. package/dist/e5496f4c01207db44ffc.svg +9 -0
  14. package/dist/playkit-transcript.js +1 -31
  15. package/dist/playkit-transcript.js.map +1 -1
  16. package/package.json +53 -50
  17. package/src/components/a11y-wrapper/a11y-wrapper.ts +26 -0
  18. package/src/components/a11y-wrapper/index.ts +1 -0
  19. package/src/components/caption/caption.scss +1 -1
  20. package/src/components/caption/caption.tsx +118 -140
  21. package/src/components/caption/index.ts +1 -1
  22. package/src/components/caption-list/captionList.scss +8 -8
  23. package/src/components/caption-list/captionList.tsx +115 -117
  24. package/src/components/caption-list/index.ts +1 -1
  25. package/src/components/close-button/close-button.scss +11 -0
  26. package/src/components/close-button/index.tsx +23 -0
  27. package/src/components/download-print-menu/download-print-menu.scss +49 -48
  28. package/src/components/download-print-menu/download-print-menu.tsx +147 -125
  29. package/src/components/download-print-menu/index.ts +1 -1
  30. package/src/components/icons/index.ts +11 -0
  31. package/src/components/plugin-button/plugin-button.scss +26 -0
  32. package/src/components/plugin-button/plugin-button.tsx +29 -0
  33. package/src/components/popover-menu/index.ts +1 -1
  34. package/src/components/popover-menu/popover-menu.scss +3 -3
  35. package/src/components/popover-menu/popover-menu.tsx +25 -25
  36. package/src/components/search/index.ts +1 -1
  37. package/src/components/search/search.scss +1 -1
  38. package/src/components/search/search.tsx +137 -144
  39. package/src/components/spinner/index.ts +1 -1
  40. package/src/components/spinner/spinner.scss +58 -50
  41. package/src/components/spinner/spinner.tsx +9 -9
  42. package/src/components/transcript/index.ts +1 -1
  43. package/src/components/transcript/transcript.scss +9 -33
  44. package/src/components/transcript/transcript.tsx +333 -454
  45. package/src/global.d.ts +6 -6
  46. package/src/index.ts +13 -1
  47. package/src/transcript-plugin.scss +3 -3
  48. package/src/transcript-plugin.tsx +210 -391
  49. package/src/types/index.ts +3 -0
  50. package/src/types/transcript-config.ts +11 -0
  51. package/src/types/transcript-item-data.ts +29 -0
  52. package/src/types/types-ui.ts +11 -0
  53. package/src/utils/debounce.ts +36 -0
  54. package/src/utils/index.ts +4 -0
  55. package/src/utils/object-utils.ts +34 -0
  56. package/src/utils/popover/popover.scss +30 -0
  57. package/src/utils/popover/popover.tsx +178 -0
  58. package/src/utils/utils.ts +86 -0
  59. package/src/variables.scss +14 -0
  60. package/src/assets/close.svg +0 -10
  61. package/src/utils.ts +0 -192
@@ -1,494 +1,373 @@
1
- import { h, Component } from "preact";
2
- import { KeyboardKeys } from "@playkit-js-contrib/ui";
3
- import * as styles from "./transcript.scss";
4
- import { getContribLogger, CuepointEngine, debounce } from "@playkit-js-contrib/common";
5
- import { Spinner } from "../spinner";
6
- import { CaptionItem } from "../../utils";
7
- import { Search } from "../search";
8
- import { CaptionList } from "../caption-list";
9
-
1
+ import {h, Component} from 'preact';
2
+ import {debounce} from '../../utils';
3
+ import * as styles from './transcript.scss';
4
+ import {Spinner} from '../spinner';
5
+ import {Search} from '../search';
6
+ import {CaptionList} from '../caption-list';
7
+ import {HighlightedMap, CuePointData, PluginPositions} from '../../types';
8
+ import {A11yWrapper} from '../a11y-wrapper';
9
+ import {CloseButton} from '../close-button';
10
+
11
+ const {ENTER, Space, Tab, Esc, click} = KalturaPlayer.ui.utils.KeyMap;
10
12
  export interface TranscriptProps {
11
- onSeek(time: number): void;
12
- onClose: () => void;
13
- onRetryLoad: () => void;
14
- isLoading: boolean;
15
- hasError: boolean;
16
- captions: CaptionItem[];
17
- showTime: boolean;
18
- currentTime: number;
19
- scrollOffset: number;
20
- searchDebounceTimeout: number;
21
- searchNextPrevDebounceTimeout: number;
22
- videoDuration: number;
23
- kitchenSinkActive: boolean;
24
- toggledWithEnter: boolean;
13
+ onSeek(time: number): void;
14
+ onClose: () => void;
15
+ onRetryLoad: () => void;
16
+ isLoading: boolean;
17
+ hasError: boolean;
18
+ captions: Array<CuePointData>;
19
+ showTime: boolean;
20
+ currentTime: number;
21
+ scrollOffset: number;
22
+ searchDebounceTimeout: number;
23
+ searchNextPrevDebounceTimeout: number;
24
+ videoDuration: number;
25
+ kitchenSinkActive: boolean;
26
+ toggledWithEnter: boolean;
27
+ highlightedMap: HighlightedMap;
28
+ pluginMode: PluginPositions;
29
+ onItemClicked: (n: number) => void;
25
30
  }
26
31
 
27
32
  interface TranscriptState {
28
- isAutoScrollEnabled: boolean;
29
- search: string;
30
- activeSearchIndex: number;
31
- searchMap: Record<number, Record<string, number>>;
32
- totalSearchResults: number;
33
- highlightedMap: Record<number, true>;
34
- searchLength: number;
35
- widgetWidth: number;
33
+ isAutoScrollEnabled: boolean;
34
+ search: string;
35
+ activeSearchIndex: number;
36
+ searchMap: Record<string, Record<string, number>>;
37
+ totalSearchResults: number;
38
+ searchLength: number;
39
+ widgetWidth: number;
36
40
  }
37
41
 
38
42
  const initialSearch = {
39
- search: "",
40
- activeSearchIndex: 1,
41
- searchMap: {},
42
- totalSearchResults: 0,
43
- searchLength: 0
43
+ search: '',
44
+ activeSearchIndex: 1,
45
+ searchMap: {},
46
+ totalSearchResults: 0,
47
+ searchLength: 0
44
48
  };
45
49
 
46
- const logger = getContribLogger({
47
- class: "Transcript",
48
- module: "transcript-plugin"
49
- });
50
-
51
50
  const SEARCHBAR_HEIGHT = 38; // height of search bar with margins
52
51
 
53
52
  export class Transcript extends Component<TranscriptProps, TranscriptState> {
54
- private _transcriptListRef: HTMLElement | null = null;
55
- private _captionListRef: any = null;
56
- private _skipTranscriptButtonRef: HTMLDivElement | null = null;
57
- private _autoscrollButtonRef: HTMLDivElement | null = null;
58
- private _preventScrollEvent: boolean = false;
59
- private _scrollToSearchMatchEnabled: boolean = false;
60
- private _widgetRootRef: HTMLElement | null = null;
61
- private _engine: CuepointEngine<CaptionItem> | null = null;
62
-
63
- private _widgetHeight: number = 0;
64
- private _topAutoscrollEdge: number = 0;
65
- private _bottomAutoscrollEdge: number = 0;
66
- private _thirdOfWidgetHeight: number = 0;
67
-
68
- private _log = (msg: string, method: string) => {
69
- logger.trace(msg, {
70
- method: method || "Method not defined"
71
- });
72
- };
73
- private _silence = false;
74
- state: TranscriptState = {
75
- isAutoScrollEnabled: true,
76
- highlightedMap: {},
77
- widgetWidth: 0,
78
- ...initialSearch
79
- };
80
-
81
- componentDidMount(): void {
82
- this._log("Creating engine", "componentDidMount");
83
- this._createEngine();
53
+ private _transcriptListRef: HTMLElement | null = null;
54
+ private _captionListRef: any = null;
55
+ private _skipTranscriptButtonRef: HTMLDivElement | null = null;
56
+ private _autoscrollButtonRef: HTMLDivElement | null = null;
57
+ private _preventScrollEvent: boolean = false;
58
+ private _scrollToSearchMatchEnabled: boolean = false;
59
+ private _widgetRootRef: HTMLElement | null = null;
60
+
61
+ private _widgetHeight: number = 0;
62
+ private _topAutoscrollEdge: number = 0;
63
+ private _bottomAutoscrollEdge: number = 0;
64
+ private _thirdOfWidgetHeight: number = 0;
65
+
66
+ state: TranscriptState = {
67
+ isAutoScrollEnabled: true,
68
+ widgetWidth: 0,
69
+ ...initialSearch
70
+ };
71
+
72
+ componentDidUpdate(previousProps: Readonly<TranscriptProps>, previousState: Readonly<TranscriptState>): void {
73
+ const {captions} = this.props;
74
+ const {search} = this.state;
75
+ if (previousProps.captions !== captions) {
76
+ this.setState({search: '', isAutoScrollEnabled: true});
84
77
  }
85
78
 
86
- componentDidUpdate(
87
- previousProps: Readonly<TranscriptProps>,
88
- previousState: Readonly<TranscriptState>
89
- ): void {
90
- const { captions, currentTime } = this.props;
91
- const { search } = this.state;
92
- if (previousProps.captions !== captions) {
93
- this._log("Re-creating engine", "componentDidUpdate");
94
- this._createEngine();
95
- this.setState({ search: "", isAutoScrollEnabled: true });
96
- }
97
-
98
- if (previousProps.currentTime !== currentTime) {
99
- this._syncVisibleTranscript();
100
- }
79
+ if (previousState.search !== search) {
80
+ this._debounced.findSearchMatches();
81
+ }
101
82
 
102
- if (previousState.search !== search) {
103
- this._debounced.findSearchMatches();
104
- }
83
+ this._setWidgetSize();
84
+ }
105
85
 
106
- this._setWidgetSize();
86
+ private _enableAutoScroll = (event: any, byKeyboard?: boolean) => {
87
+ event.preventDefault();
88
+ if (this.state.isAutoScrollEnabled) {
89
+ return;
107
90
  }
108
-
109
- componentWillUnmount(): void {
110
- this._log("Removing engine", "componentWillUnmount");
111
- this._engine = null;
91
+ this._preventScrollEvent = true;
92
+ this.setState({
93
+ isAutoScrollEnabled: true
94
+ });
95
+ if (event.type !== 'click') {
96
+ this._skipTranscriptButtonRef?.focus();
112
97
  }
98
+ };
113
99
 
114
- private _createEngine = () => {
115
- const { captions } = this.props;
116
- if (!captions || captions.length === 0) {
117
- this._engine = null;
118
- return;
119
- }
120
- this._engine = new CuepointEngine<CaptionItem>(captions);
121
- this._syncVisibleTranscript();
122
- };
123
-
124
- private _syncVisibleTranscript = (forceSnapshot = false) => {
125
- const { currentTime } = this.props;
126
- this.setState((state: TranscriptState) => {
127
- if (!this._engine) {
128
- return {
129
- highlightedMap: {}
130
- };
131
- }
132
-
133
- const transcriptUpdate = this._engine.updateTime(currentTime, forceSnapshot);
134
- if (transcriptUpdate.snapshot) {
135
- this._silence = false;
136
- const highlightedMap = transcriptUpdate.snapshot.reduce((acc, item) => {
137
- return { ...acc, [item.id]: true };
138
- }, {});
139
- return {
140
- highlightedMap
141
- };
142
- }
143
-
144
- if (!transcriptUpdate.delta) {
145
- return state;
146
- }
147
-
148
- const { show, hide } = transcriptUpdate.delta;
149
-
150
- if (show.length === 0 && hide.length > 0) {
151
- this._silence = true;
152
- return state;
153
- }
154
-
155
- if (show.length > 0 || hide.length > 0) {
156
- let newHighlightedMap: Record<number, true> = {};
157
- if (this._silence) {
158
- this._silence = false;
159
- } else {
160
- newHighlightedMap = { ...state.highlightedMap };
161
- }
162
- show.forEach((caption: CaptionItem) => {
163
- newHighlightedMap[caption.id] = true;
164
- });
165
-
166
- hide.forEach((caption: CaptionItem) => {
167
- delete newHighlightedMap[caption.id];
168
- });
169
-
170
- return {
171
- highlightedMap: newHighlightedMap
172
- };
173
- }
174
- return state;
175
- });
176
- };
177
-
178
- private _enableAutoScroll = (event: any) => {
179
- event.preventDefault();
180
- if (this.state.isAutoScrollEnabled) {
181
- return;
182
- }
183
- if (
184
- event.type === "click" ||
185
- event.keyCode === KeyboardKeys.Space ||
186
- event.keyCode === KeyboardKeys.Enter
187
- ) {
188
- this._preventScrollEvent = true;
189
- this.setState({
190
- isAutoScrollEnabled: true
191
- });
192
- if (event.type !== "click") {
193
- this._skipTranscriptButtonRef?.focus();
194
- }
100
+ private _renderScrollToButton = () => {
101
+ const {isAutoScrollEnabled} = this.state;
102
+ return (
103
+ <A11yWrapper onClick={this._enableAutoScroll}>
104
+ <div
105
+ role="button"
106
+ className={`${styles.autoscrollButton} ${isAutoScrollEnabled ? '' : styles.autoscrollButtonVisible}`}
107
+ tabIndex={isAutoScrollEnabled ? -1 : 1}
108
+ ref={node => {
109
+ this._autoscrollButtonRef = node;
110
+ }}
111
+ />
112
+ </A11yWrapper>
113
+ );
114
+ };
115
+
116
+ private _onSearch = (search: string) => {
117
+ this.setState({search});
118
+ };
119
+
120
+ private _findSearchMatches = () => {
121
+ this.setState((state: TranscriptState) => {
122
+ if (!state.search) {
123
+ return {...initialSearch};
124
+ }
125
+ let index = 0;
126
+ const loSearch = state.search.toLowerCase();
127
+ const searchMap: Record<string, Record<string, number>> = {};
128
+ this.props.captions.forEach((caption: CuePointData) => {
129
+ const text = caption?.text?.toLowerCase() || '';
130
+ const regex = new RegExp(loSearch, 'gi');
131
+ let result;
132
+ const indices = [];
133
+ while ((result = regex.exec(text))) {
134
+ indices.push(result.index);
195
135
  }
196
- };
197
-
198
- private _renderScrollToButton = () => {
199
- const { isAutoScrollEnabled } = this.state;
200
- return (
201
- <div
202
- role="button"
203
- className={`${styles.autoscrollButton} ${isAutoScrollEnabled ? "" : styles.autoscrollButtonVisible}`}
204
- onClick={this._enableAutoScroll}
205
- onKeyDown={this._enableAutoScroll}
206
- tabIndex={isAutoScrollEnabled ? -1 : 1}
207
- ref={node => {
208
- this._autoscrollButtonRef = node;
209
- }}
210
- />
211
- );
212
- };
213
-
214
- private _onSearch = (search: string) => {
215
- this.setState({ search });
216
- };
217
-
218
- private _findSearchMatches = () => {
219
- this.setState((state: TranscriptState) => {
220
- if (!state.search) {
221
- return { ...initialSearch };
222
- }
223
- let index = 0;
224
- const loSearch = state.search.toLowerCase();
225
- const searchMap: Record<number, Record<number, number>> = {};
226
- this.props.captions.forEach((caption: CaptionItem) => {
227
- const text = caption.text.toLowerCase();
228
- const regex = new RegExp(loSearch, "gi");
229
- let result;
230
- const indices = [];
231
- while ((result = regex.exec(text))) {
232
- indices.push(result.index);
233
- }
234
- indices.forEach((i: number) => {
235
- index++;
236
- searchMap[caption.id] = { ...searchMap[caption.id], [index]: i };
237
- });
238
- });
239
- return {
240
- searchMap,
241
- totalSearchResults: index,
242
- activeSearchIndex: 1,
243
- searchLength: loSearch.length
244
- };
136
+ indices.forEach((i: number) => {
137
+ index++;
138
+ searchMap[caption.id] = {...searchMap[caption.id], [index]: i};
245
139
  });
246
- };
247
-
248
- private _setActiveSearchIndex = (index: number) => {
249
- this._scrollToSearchMatchEnabled = true;
250
- this.setState({
251
- activeSearchIndex: index,
252
- isAutoScrollEnabled: false
253
- });
254
- };
255
-
256
- private _getHeaderStyles = (): string => {
257
- const { widgetWidth } = this.state;
258
- if (widgetWidth >= 692) {
259
- return "";
260
- }
261
- if (widgetWidth >= 649) {
262
- return styles.mediumWidth;
263
- }
264
- return styles.smallWidth;
140
+ });
141
+ return {
142
+ searchMap,
143
+ totalSearchResults: index,
144
+ activeSearchIndex: 1,
145
+ searchLength: loSearch.length
146
+ };
147
+ });
148
+ };
149
+
150
+ private _setActiveSearchIndex = (index: number) => {
151
+ this._scrollToSearchMatchEnabled = true;
152
+ this.setState({
153
+ activeSearchIndex: index,
154
+ isAutoScrollEnabled: false
155
+ });
156
+ };
157
+
158
+ private _getHeaderStyles = (): string => {
159
+ const {widgetWidth} = this.state;
160
+ if (widgetWidth >= 692) {
161
+ return '';
265
162
  }
266
-
267
- private _renderHeader = () => {
268
- const { toggledWithEnter, kitchenSinkActive } = this.props;
269
- const { search, activeSearchIndex, totalSearchResults } = this.state;
270
- return (
271
- <div className={[styles.header, this._getHeaderStyles()].join(' ')}>
272
- <Search
273
- onChange={this._onSearch}
274
- onSearchIndexChange={this._debounced.onActiveSearchIndexChange}
275
- value={search}
276
- activeSearchIndex={activeSearchIndex}
277
- totalSearchResults={totalSearchResults}
278
- toggledWithEnter={toggledWithEnter}
279
- kitchenSinkActive={kitchenSinkActive}
280
- />
281
- </div>
282
- );
283
- };
284
-
285
- private _handleKeyDown = (event: KeyboardEvent) => {
286
- if (event.keyCode === KeyboardKeys.Tab && !event.shiftKey) {
287
- this.setState({
288
- isAutoScrollEnabled: false
289
- });
290
- const captionRef = this._captionListRef?._currentCaptionRef?._hotspotRef;
291
- if (captionRef) {
292
- event.preventDefault();
293
- captionRef.focus();
294
- }
295
- } else if (
296
- event.keyCode === KeyboardKeys.Enter ||
297
- event.keyCode === KeyboardKeys.Space
298
- ) {
163
+ if (widgetWidth >= 649) {
164
+ return styles.mediumWidth;
165
+ }
166
+ return styles.smallWidth;
167
+ };
168
+
169
+ private _renderHeader = () => {
170
+ const {toggledWithEnter, kitchenSinkActive} = this.props;
171
+ const {search, activeSearchIndex, totalSearchResults} = this.state;
172
+ return (
173
+ <div className={[styles.header, this._getHeaderStyles()].join(' ')}>
174
+ <Search
175
+ onChange={this._onSearch}
176
+ onSearchIndexChange={this._debounced.onActiveSearchIndexChange}
177
+ value={search}
178
+ activeSearchIndex={activeSearchIndex}
179
+ totalSearchResults={totalSearchResults}
180
+ toggledWithEnter={toggledWithEnter}
181
+ kitchenSinkActive={kitchenSinkActive}
182
+ searchQuery={search}
183
+ />
184
+ </div>
185
+ );
186
+ };
187
+
188
+ private _handleKeyDown = (event: KeyboardEvent) => {
189
+ if (event.keyCode === Tab && !event.shiftKey) {
190
+ this.setState({
191
+ isAutoScrollEnabled: false
192
+ });
193
+ const captionRef = this._captionListRef?._currentCaptionRef?._hotspotRef;
194
+ if (captionRef) {
299
195
  event.preventDefault();
300
- this._autoscrollButtonRef?.focus();
196
+ captionRef.focus();
301
197
  }
302
- };
198
+ } else if (event.keyCode === ENTER || event.keyCode === Space) {
199
+ event.preventDefault();
200
+ this._autoscrollButtonRef?.focus();
201
+ }
202
+ };
203
+
204
+ private _renderSkipTranscriptButton = () => {
205
+ return (
206
+ <div
207
+ role="button"
208
+ ref={node => {
209
+ this._skipTranscriptButtonRef = node;
210
+ }}
211
+ className={styles.skipTranscriptButton}
212
+ onKeyDown={this._handleKeyDown}
213
+ tabIndex={1}
214
+ >
215
+ Skip transcript
216
+ </div>
217
+ );
218
+ };
219
+
220
+ private _renderTranscript = () => {
221
+ const {captions, hasError, onRetryLoad, showTime, videoDuration, highlightedMap} = this.props;
222
+ const {isAutoScrollEnabled, searchMap, activeSearchIndex, searchLength} = this.state;
223
+ if (!captions || !captions.length) {
224
+ return null;
225
+ }
303
226
 
304
- private _renderSkipTranscriptButton = () => {
227
+ if (hasError) {
305
228
  return (
306
- <div
307
- role="button"
308
- ref={(node) => {
309
- this._skipTranscriptButtonRef = node;
310
- }}
311
- className={styles.skipTranscriptButton}
312
- onKeyDown={this._handleKeyDown}
313
- tabIndex={1}
314
- >
315
- Skip transcript
229
+ <div className={styles.errorWrapper}>
230
+ <div className={styles.errorIcon} />
231
+ <p className={styles.errorMainText}>Whoops!</p>
232
+ <p className={styles.errorDescriptionText}>
233
+ Failed to get transcript, please try again
234
+ <button className={styles.retryButton} onClick={onRetryLoad}>
235
+ Retry
236
+ </button>
237
+ </p>
316
238
  </div>
317
239
  );
318
- };
319
-
320
- private _renderTranscript = () => {
321
- const { captions, hasError, onRetryLoad, showTime, videoDuration } = this.props;
322
- const {
323
- isAutoScrollEnabled,
324
- searchMap,
325
- activeSearchIndex,
326
- highlightedMap,
327
- searchLength
328
- } = this.state;
329
- if (!captions || !captions.length) {
330
- return null;
331
- }
332
-
333
- if (hasError) {
334
- return (
335
- <div className={styles.errorWrapper}>
336
- <div className={styles.errorIcon} />
337
- <p className={styles.errorMainText}>Whoops!</p>
338
- <p className={styles.errorDescriptionText}>
339
- Failed to get transcript, please try again
340
- {/* <button
341
- className={styles.retryButton}
342
- onClick={onRetryLoad}
343
- >
344
- Retry
345
- </button> */}
346
- </p>
347
- </div>
348
- );
349
- }
350
-
351
- const captionProps = {
352
- showTime,
353
- searchLength,
354
- scrollTo: this._scrollTo,
355
- scrollToSearchMatch: this._scrollToSearchMatch,
356
- videoDuration
357
- };
358
-
359
- return (
360
- <CaptionList
361
- ref={node => {
362
- this._captionListRef = node;
363
- }}
364
- highlightedMap={highlightedMap}
365
- captions={captions}
366
- seekTo={this._handleSeek}
367
- isAutoScrollEnabled={isAutoScrollEnabled}
368
- searchMap={searchMap}
369
- activeSearchIndex={activeSearchIndex}
370
- captionProps={captionProps}
371
- />
372
- );
373
- };
374
-
375
- private _setWidgetSize = () => {
376
- if (this._widgetRootRef) {
377
- const { width, height, top } = this._widgetRootRef.getBoundingClientRect();
378
- if (this.state.widgetWidth !== width) {
379
- this.setState({
380
- widgetWidth: width
381
- })
382
- }
383
- if (this._widgetHeight !== height) {
384
- this._widgetHeight = height;
385
- this._thirdOfWidgetHeight = height / 3;
386
- this._topAutoscrollEdge = Math.floor(this._thirdOfWidgetHeight + SEARCHBAR_HEIGHT);
387
- this._bottomAutoscrollEdge = Math.ceil((this._thirdOfWidgetHeight * 2) + SEARCHBAR_HEIGHT);
388
- }
389
- }
390
240
  }
391
241
 
392
- private _renderLoading = () => {
393
- return (
394
- <div className={styles.loadingWrapper}>
395
- <Spinner />
396
- </div>
397
- );
242
+ const captionProps = {
243
+ showTime,
244
+ searchLength,
245
+ scrollTo: this._scrollTo,
246
+ scrollToSearchMatch: this._scrollToSearchMatch,
247
+ videoDuration
398
248
  };
399
249
 
400
- private _scrollTo = (el: HTMLElement) => {
401
- if (this._transcriptListRef) {
402
- this._preventScrollEvent = true;
403
- const { top } = el.getBoundingClientRect();
404
- if (top >= this._topAutoscrollEdge || top <= this._bottomAutoscrollEdge) {
405
- this._transcriptListRef.scrollTop = el.offsetTop - (this._thirdOfWidgetHeight - SEARCHBAR_HEIGHT);
406
- }
407
- }
408
- };
409
-
410
- private _scrollToSearchMatch = (el: HTMLElement) => {
411
- if (this._transcriptListRef && this._scrollToSearchMatchEnabled) {
412
- this._scrollToSearchMatchEnabled = false;
413
- this._preventScrollEvent = true;
414
- this._transcriptListRef.scrollTop = el.offsetTop - SEARCHBAR_HEIGHT;
415
- }
416
- }
417
-
418
- private _onScroll = () => {
419
- if (this._preventScrollEvent) {
420
- this._preventScrollEvent = false;
421
- return;
422
- }
250
+ return (
251
+ <CaptionList
252
+ ref={node => {
253
+ this._captionListRef = node;
254
+ }}
255
+ highlightedMap={highlightedMap}
256
+ data={captions}
257
+ onSeek={this._handleSeek}
258
+ isAutoScrollEnabled={isAutoScrollEnabled}
259
+ searchMap={searchMap}
260
+ activeSearchIndex={activeSearchIndex}
261
+ captionProps={captionProps}
262
+ onScroll={this._onScroll}
263
+ widgetWidth={this.state.widgetWidth}
264
+ showItemsIcons={true}
265
+ searchActive={false}
266
+ />
267
+ );
268
+ };
269
+
270
+ private _setWidgetSize = () => {
271
+ if (this._widgetRootRef) {
272
+ const {width, height, top} = this._widgetRootRef.getBoundingClientRect();
273
+ if (this.state.widgetWidth !== width) {
423
274
  this.setState({
424
- isAutoScrollEnabled: false
275
+ widgetWidth: width
425
276
  });
426
- };
427
-
428
- private _handleSeek = (caption: CaptionItem) => {
429
- const { onSeek } = this.props;
430
- const selection = window.getSelection();
431
- if (selection && selection.type !== "Range") {
432
- onSeek(caption.startTime);
433
- }
434
- };
277
+ }
278
+ if (this._widgetHeight !== height) {
279
+ this._widgetHeight = height;
280
+ this._thirdOfWidgetHeight = height / 3;
281
+ this._topAutoscrollEdge = Math.floor(this._thirdOfWidgetHeight + SEARCHBAR_HEIGHT);
282
+ this._bottomAutoscrollEdge = Math.ceil(this._thirdOfWidgetHeight * 2 + SEARCHBAR_HEIGHT);
283
+ }
284
+ }
285
+ };
286
+
287
+ private _renderLoading = () => {
288
+ return (
289
+ <div className={styles.loadingWrapper}>
290
+ <Spinner />
291
+ </div>
292
+ );
293
+ };
294
+
295
+ private _scrollTo = (el: HTMLElement) => {
296
+ if (this._transcriptListRef) {
297
+ this._preventScrollEvent = true;
298
+ const {top} = el.getBoundingClientRect();
299
+ if (top >= this._topAutoscrollEdge || top <= this._bottomAutoscrollEdge) {
300
+ this._transcriptListRef.scrollTop = el.offsetTop - (this._thirdOfWidgetHeight - SEARCHBAR_HEIGHT);
301
+ }
302
+ }
303
+ };
435
304
 
436
- private _debounced = {
437
- findSearchMatches: debounce(this._findSearchMatches, this.props.searchDebounceTimeout),
438
- onActiveSearchIndexChange: debounce(
439
- this._setActiveSearchIndex,
440
- this.props.searchNextPrevDebounceTimeout
441
- )
442
- };
305
+ private _scrollToSearchMatch = (el: HTMLElement) => {
306
+ if (this._transcriptListRef && this._scrollToSearchMatchEnabled) {
307
+ this._scrollToSearchMatchEnabled = false;
308
+ this._preventScrollEvent = true;
309
+ this._transcriptListRef.scrollTop = el.offsetTop - SEARCHBAR_HEIGHT;
310
+ }
311
+ };
443
312
 
444
- private _handleEsc = (event: KeyboardEvent) => {
445
- if (event.keyCode === KeyboardKeys.Esc) {
446
- this.props.onClose();
447
- }
448
- };
313
+ private _onScroll = () => {
314
+ if (this._preventScrollEvent) {
315
+ this._preventScrollEvent = false;
316
+ return;
317
+ }
318
+ this.setState({
319
+ isAutoScrollEnabled: false
320
+ });
321
+ };
322
+
323
+ private _handleSeek = (caption: CuePointData) => {
324
+ const {onSeek} = this.props;
325
+ const selection = window.getSelection();
326
+ if (selection && selection.type !== 'Range') {
327
+ onSeek(caption.startTime);
328
+ }
329
+ };
449
330
 
450
- private _handleClose = (event: any) => {
451
- if (
452
- event.type === "click" ||
453
- event.keyCode === KeyboardKeys.Space ||
454
- event.keyCode === KeyboardKeys.Enter
455
- ) {
456
- this.props.onClose();
457
- }
458
- };
331
+ private _debounced = {
332
+ findSearchMatches: debounce(this._findSearchMatches, this.props.searchDebounceTimeout),
333
+ onActiveSearchIndexChange: debounce(this._setActiveSearchIndex, this.props.searchNextPrevDebounceTimeout)
334
+ };
459
335
 
460
- render(props: TranscriptProps) {
461
- const { isLoading, kitchenSinkActive } = props;
462
- return (
463
- <div
464
- className={`${styles.root} ${kitchenSinkActive ? '' : styles.hidden}`}
465
- ref={node => {
466
- this._widgetRootRef = node;
467
- }}
468
- onKeyUp={this._handleEsc}
469
- >
470
- <div className={styles.globalContainer}>
471
- {this._renderHeader()}
472
- <div
473
- role="button"
474
- className={styles.closeButton}
475
- tabIndex={1}
476
- onClick={this._handleClose}
477
- onKeyUp={this._handleClose}
478
- />
479
- {!isLoading && this._renderSkipTranscriptButton()}
480
- <div
481
- className={styles.body}
482
- onScroll={this._onScroll}
483
- ref={node => {
484
- this._transcriptListRef = node;
485
- }}
486
- >
487
- {isLoading ? this._renderLoading() : this._renderTranscript()}
488
- </div>
489
- {this._renderScrollToButton()}
490
- </div>
491
- </div>
492
- );
336
+ private _handleEsc = (event: KeyboardEvent) => {
337
+ if (event.keyCode === Esc) {
338
+ this.props.onClose();
493
339
  }
340
+ };
341
+
342
+ render(props: TranscriptProps) {
343
+ const {isLoading, kitchenSinkActive} = props;
344
+
345
+ return (
346
+ <div
347
+ className={`${styles.root} ${kitchenSinkActive ? '' : styles.hidden}`}
348
+ ref={node => {
349
+ this._widgetRootRef = node;
350
+ }}
351
+ onKeyUp={this._handleEsc}
352
+ >
353
+ <div className={styles.globalContainer}>
354
+ {this._renderHeader()}
355
+
356
+ <CloseButton onClick={this.props.onClose} />
357
+
358
+ {!isLoading && this._renderSkipTranscriptButton()}
359
+ <div
360
+ className={styles.body}
361
+ onScroll={this._onScroll}
362
+ ref={node => {
363
+ this._transcriptListRef = node;
364
+ }}
365
+ >
366
+ {isLoading ? this._renderLoading() : this._renderTranscript()}
367
+ </div>
368
+ {this._renderScrollToButton()}
369
+ </div>
370
+ </div>
371
+ );
372
+ }
494
373
  }