@playkit-js/transcript 3.5.25 → 3.5.26-canary.0-ef2a1f1

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/transcript",
3
- "version": "3.5.25",
3
+ "version": "3.5.26-canary.0-ef2a1f1",
4
4
  "main": "dist/playkit-transcript.js",
5
5
  "license": "AGPL-3.0",
6
6
  "private": false,
@@ -21,7 +21,7 @@
21
21
  "copyfiles": "^2.4.1",
22
22
  "cross-env": "^7.0.3",
23
23
  "css-loader": "^6.7.1",
24
- "cypress": "^12.12.0",
24
+ "cypress": "13.13.1",
25
25
  "playwright-webkit": "^1.33.0",
26
26
  "prettier": "^2.6.2",
27
27
  "rimraf": "^5.0.5",
@@ -4,7 +4,11 @@ import {secondsToTime} from '../../utils';
4
4
  import {CuePointData} from '../../types';
5
5
  import * as styles from './caption.scss';
6
6
 
7
+ import {TranscriptEvents} from '../../events/events';
8
+
7
9
  const {withText, Text} = KalturaPlayer.ui.preacti18n;
10
+ const {withEventManager} = KalturaPlayer.ui.Event;
11
+ const {withPlayer} = KalturaPlayer.ui.components;
8
12
 
9
13
  export interface CaptionProps {
10
14
  showTime: boolean;
@@ -12,7 +16,11 @@ export interface CaptionProps {
12
16
  scrollTo(el: HTMLElement): void;
13
17
  scrollToSearchMatch(el: HTMLElement): void;
14
18
  videoDuration: number;
19
+ eventManager?: any;
20
+ player?: any;
15
21
  captionLabel?: string;
22
+ moveToSearch?: string;
23
+ setTextToRead: (textToRead: string, delay?: number) => void;
16
24
  }
17
25
 
18
26
  interface ExtendedCaptionProps extends CaptionProps {
@@ -28,21 +36,45 @@ interface ExtendedCaptionProps extends CaptionProps {
28
36
  }
29
37
 
30
38
  const translates = {
31
- captionLabel: <Text id="transcript.caption_label">Jump to this point in video</Text>
39
+ captionLabel: <Text id="transcript.caption_label">Jump to this point in video</Text>,
40
+ moveToSearch: <Text id="transcript.move_to_search">Click to jump to search result</Text>
32
41
  };
33
42
 
34
43
  @withText(translates)
44
+ @withEventManager
45
+ @withPlayer
35
46
  export class Caption extends Component<ExtendedCaptionProps> {
36
- private _hotspotRef: HTMLElement | null = null;
47
+ private _captionRef: HTMLElement | null = null;
48
+
49
+ get indexArray() {
50
+ if (!this.props.indexMap) {
51
+ return [];
52
+ }
53
+ return Object.keys(this.props.indexMap).sort((a, b) => Number(a) - Number(b));
54
+ }
37
55
 
38
- componentDidUpdate() {
39
- if (this._hotspotRef && this.props.shouldScroll) {
40
- this.props.scrollTo(this._hotspotRef);
41
- } else if (this._hotspotRef && this.props.shouldScrollToSearchMatch) {
42
- this.props.scrollToSearchMatch(this._hotspotRef);
56
+ componentDidUpdate(previousProps: Readonly<ExtendedCaptionProps>) {
57
+ if (this._captionRef && this.props.shouldScroll) {
58
+ this.props.scrollTo(this._captionRef);
59
+ } else if (this._captionRef && this.props.shouldScrollToSearchMatch) {
60
+ this.props.scrollToSearchMatch(this._captionRef);
61
+ }
62
+ if (this.props.indexMap && previousProps.activeSearchIndex !== this.props.activeSearchIndex) {
63
+ if (this._hasSearchMatch()) {
64
+ this.props.setTextToRead(this.props.caption.text);
65
+ }
43
66
  }
44
67
  }
45
68
 
69
+ componentDidMount(): void {
70
+ const {eventManager, player} = this.props;
71
+ eventManager?.listen(player, TranscriptEvents.TRANSCRIPT_TO_SEARCH_MATCH, () => {
72
+ if (this._hasSearchMatch()) {
73
+ this._captionRef?.focus();
74
+ }
75
+ });
76
+ }
77
+
46
78
  shouldComponentUpdate(nextProps: ExtendedCaptionProps) {
47
79
  const {indexMap, highlighted, isAutoScrollEnabled, activeSearchIndex, longerThanHour} = this.props;
48
80
  if (longerThanHour !== nextProps.longerThanHour) {
@@ -74,12 +106,16 @@ export class Caption extends Component<ExtendedCaptionProps> {
74
106
  }
75
107
  };
76
108
 
109
+ private _hasSearchMatch = () => {
110
+ if (!this.props.indexMap) {
111
+ return false;
112
+ }
113
+ return Boolean(this.indexArray.find((el: string) => Number(el) === this.props.activeSearchIndex));
114
+ };
115
+
77
116
  private _renderText = (text: string) => {
78
117
  const {activeSearchIndex, searchLength, indexMap} = this.props;
79
- let indexArray: string[] = [];
80
- if (indexMap) {
81
- indexArray = Object.keys(indexMap).sort((a, b) => Number(a) - Number(b));
82
- }
118
+ const indexArray = this.indexArray;
83
119
  if (text?.length === 0) {
84
120
  return null;
85
121
  }
@@ -107,15 +143,15 @@ export class Caption extends Component<ExtendedCaptionProps> {
107
143
  };
108
144
 
109
145
  render() {
110
- const {caption, highlighted, showTime, longerThanHour} = this.props;
146
+ const {caption, highlighted, showTime, longerThanHour, indexMap, captionLabel, moveToSearch} = this.props;
111
147
  const {startTime, id} = caption;
112
- const isHighlighted = Object.keys(highlighted).some(c => c === id) ;
148
+ const isHighlighted = Object.keys(highlighted).some(c => c === id);
113
149
  const time = showTime ? secondsToTime(startTime, longerThanHour) : '';
114
150
 
115
151
  const captionA11yProps: Record<string, any> = {
116
152
  ariaCurrent: isHighlighted,
117
153
  tabIndex: 0,
118
- ariaLabel: `${time}${showTime ? ' ' : ''}${caption.text} ${this.props.captionLabel}`,
154
+ ariaLabel: `${time}${showTime ? ' ' : ''}${caption.text} ${indexMap ? moveToSearch : captionLabel}`,
119
155
  role: 'button'
120
156
  };
121
157
 
@@ -124,7 +160,7 @@ export class Caption extends Component<ExtendedCaptionProps> {
124
160
  <div
125
161
  className={styles.caption}
126
162
  ref={node => {
127
- this._hotspotRef = node;
163
+ this._captionRef = node;
128
164
  }}
129
165
  {...captionA11yProps}>
130
166
  {showTime && (
@@ -1,4 +1,5 @@
1
1
  import {h, Component} from 'preact';
2
+ import {ScreenReaderContext} from '@playkit-js/common/dist/hoc/sr-wrapper';
2
3
  import {HOUR} from '../../utils';
3
4
  import {Caption} from '../caption';
4
5
  import * as styles from './captionList.scss';
@@ -111,27 +112,34 @@ export class CaptionList extends Component<Props> {
111
112
  {data.map((captionData, index) => {
112
113
  const captionProps = this._getCaptionProps(captionData);
113
114
  return (
114
- <Caption
115
- ref={node => {
116
- if (index === 0) {
117
- this._firstCaptionRef = node;
118
- } else if (index === data.length - 1) {
119
- this._lastCaptionRef = node;
120
- }
121
- if (captionProps.searchCaption){
122
- Object.keys(captionProps.searchCaption).forEach(key => {
123
- if (parseInt(key) === this.props.activeSearchIndex) {
124
- this._currentCaptionRef = node
125
- isSearchCaption = true;
126
- }
127
- });
128
- }
129
- if (!isSearchCaption && captionProps.highlighted[captionData.id]) {
130
- this._currentCaptionRef = node;
131
- }
115
+ <ScreenReaderContext.Consumer>
116
+ {(setTextToRead: (textToRead: string, delay?: number | undefined) => void) => {
117
+ return (
118
+ <Caption
119
+ setTextToRead={setTextToRead}
120
+ ref={node => {
121
+ if (index === 0) {
122
+ this._firstCaptionRef = node;
123
+ } else if (index === data.length - 1) {
124
+ this._lastCaptionRef = node;
125
+ }
126
+ if (captionProps.searchCaption) {
127
+ Object.keys(captionProps.searchCaption).forEach(key => {
128
+ if (parseInt(key) === this.props.activeSearchIndex) {
129
+ this._currentCaptionRef = node;
130
+ isSearchCaption = true;
131
+ }
132
+ });
133
+ }
134
+ if (!isSearchCaption && captionProps.highlighted[captionData.id]) {
135
+ this._currentCaptionRef = node;
136
+ }
137
+ }}
138
+ {...captionProps}
139
+ />
140
+ );
132
141
  }}
133
- {...captionProps}
134
- />
142
+ </ScreenReaderContext.Consumer>
135
143
  );
136
144
  })}
137
145
  </div>
@@ -1,5 +1,7 @@
1
1
  @import '../../variables.scss';
2
2
 
3
+ $button-height: 32px;
4
+
3
5
  .hidden {
4
6
  visibility: hidden;
5
7
  }
@@ -25,7 +27,7 @@
25
27
  align-items: center;
26
28
  justify-content: center;
27
29
  width: 120px;
28
- height: 32px;
30
+ height: $button-height;
29
31
  border-radius: $roundness-3;
30
32
  box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3);
31
33
  border: solid 1px $primary-color;
@@ -60,16 +62,28 @@
60
62
  }
61
63
 
62
64
  .header {
65
+ $header-margin-bottom: 8px;
66
+
67
+ position: relative;
63
68
  display: flex;
64
69
  align-items: center;
65
70
  justify-content: space-between;
66
71
  width: 100%;
67
- margin-bottom: 8px;
72
+ margin-bottom: $header-margin-bottom;
68
73
  padding-left: 16px;
69
74
  font-size: 16px;
70
75
  padding-right: 16px;
71
76
  gap: 8px;
72
77
  z-index: 2;
78
+ .to-search-button {
79
+ position: absolute;
80
+ right: 16px;
81
+ bottom: calc((#{$button-height + $header-margin-bottom}) * -1); // button height + margin
82
+ opacity: 0;
83
+ &:focus {
84
+ opacity: 1;
85
+ }
86
+ }
73
87
  }
74
88
 
75
89
  .body {
@@ -11,6 +11,7 @@ import {AutoscrollButton} from '../autoscroll-button';
11
11
  import {TranscriptMenu} from '../transcript-menu';
12
12
  import {SmallScreenSlate} from '../small-screen-slate';
13
13
  import {Button, ButtonType, ButtonSize} from '@playkit-js/common/dist/components/button';
14
+ import {ScreenReaderProvider} from '@playkit-js/common/dist/hoc/sr-wrapper';
14
15
  import {OnClickEvent, OnClick} from '@playkit-js/common/dist/hoc/a11y-wrapper';
15
16
  import {TranscriptEvents} from '../../events/events';
16
17
 
@@ -28,7 +29,9 @@ const translates = {
28
29
  errorTitle: <Text id="transcript.whoops">Whoops!</Text>,
29
30
  errorDescripton: <Text id="transcript.load_failed">Failed to load transcript</Text>,
30
31
  attachTranscript: <Text id="transcript.attach_transcript">Bring Transcript back</Text>,
31
- detachTranscript: <Text id="transcript.detach_transcript">Popout transcript</Text>
32
+ detachTranscript: <Text id="transcript.detach_transcript">Popout transcript</Text>,
33
+ toSearchResult: <Text id="transcript.to_search_result">Go to result</Text>,
34
+ toSearchResultLabel: <Text id="transcript.to_search_result_label">Click to jump to this point in the video</Text>
32
35
  };
33
36
 
34
37
  export interface TranscriptProps {
@@ -53,6 +56,8 @@ export interface TranscriptProps {
53
56
  errorDescripton?: string;
54
57
  attachTranscript?: string;
55
58
  detachTranscript?: string;
59
+ toSearchResult?: string;
60
+ toSearchResultLabel?: string;
56
61
  downloadDisabled: boolean;
57
62
  onDownload: () => void;
58
63
  printDisabled: boolean;
@@ -66,6 +71,7 @@ export interface TranscriptProps {
66
71
  kitchenSinkDetached: boolean;
67
72
  isMobile?: boolean;
68
73
  playerWidth?: number;
74
+ onJumpToSearchMatch: () => void;
69
75
  }
70
76
 
71
77
  interface TranscriptState {
@@ -280,8 +286,11 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
280
286
  isLoading,
281
287
  attachTranscript,
282
288
  detachTranscript,
289
+ toSearchResult,
290
+ toSearchResultLabel,
283
291
  onAttach,
284
- onDetach
292
+ onDetach,
293
+ onJumpToSearchMatch
285
294
  } = this.props;
286
295
  const {search, activeSearchIndex, totalSearchResults} = this.state;
287
296
 
@@ -294,7 +303,6 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
294
303
  disabled: isLoading
295
304
  };
296
305
  }
297
-
298
306
  return (
299
307
  <div className={[styles.header, this._getHeaderStyles()].join(' ')} data-testid="transcript_header">
300
308
  <Search
@@ -306,6 +314,16 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
306
314
  toggledWithEnter={toggledWithEnter}
307
315
  kitchenSinkActive={kitchenSinkActive}
308
316
  />
317
+ {search && activeSearchIndex && (
318
+ <Button
319
+ type={ButtonType.secondary}
320
+ className={styles.toSearchButton}
321
+ onClick={onJumpToSearchMatch}
322
+ ariaLabel={toSearchResultLabel}
323
+ testId="transcript_jumpToSearchMatch">
324
+ {toSearchResult}
325
+ </Button>
326
+ )}
309
327
  <TranscriptMenu {...{downloadDisabled, onDownload, printDisabled, onPrint, isLoading, detachMenuItem, kitchenSinkDetached}} />
310
328
  {!detachMenuItem && this._renderDetachButton()}
311
329
  {!kitchenSinkDetached && (
@@ -477,32 +495,34 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
477
495
  const {isLoading, kitchenSinkActive, kitchenSinkDetached, hasError, smallScreen, toggledWithEnter} = props;
478
496
  const renderTranscriptButtons = !(isLoading || hasError);
479
497
  return (
480
- <div
481
- className={`${styles.root} ${kitchenSinkActive || kitchenSinkDetached ? '' : styles.hidden}`}
482
- ref={node => {
483
- this._widgetRootRef = node;
484
- }}
485
- data-testid="transcript_root">
486
- {smallScreen && !kitchenSinkDetached ? (
487
- <SmallScreenSlate onClose={this.props.onClose} toggledWithEnter={toggledWithEnter} />
488
- ) : (
489
- <div className={styles.globalContainer}>
490
- {this._renderHeader()}
491
-
492
- {renderTranscriptButtons && this._renderSkipTranscriptButton()}
493
- <div
494
- className={styles.body}
495
- onScroll={this._onScroll}
496
- ref={node => {
497
- this._transcriptListRef = node;
498
- }}
499
- data-testid="transcript_list">
500
- {isLoading ? this._renderLoading() : this._renderTranscript()}
498
+ <ScreenReaderProvider>
499
+ <div
500
+ className={`${styles.root} ${kitchenSinkActive || kitchenSinkDetached ? '' : styles.hidden}`}
501
+ ref={node => {
502
+ this._widgetRootRef = node;
503
+ }}
504
+ data-testid="transcript_root">
505
+ {smallScreen && !kitchenSinkDetached ? (
506
+ <SmallScreenSlate onClose={this.props.onClose} toggledWithEnter={toggledWithEnter} />
507
+ ) : (
508
+ <div className={styles.globalContainer}>
509
+ {this._renderHeader()}
510
+
511
+ {renderTranscriptButtons && this._renderSkipTranscriptButton()}
512
+ <div
513
+ className={styles.body}
514
+ onScroll={this._onScroll}
515
+ ref={node => {
516
+ this._transcriptListRef = node;
517
+ }}
518
+ data-testid="transcript_list">
519
+ {isLoading ? this._renderLoading() : this._renderTranscript()}
520
+ </div>
521
+ {renderTranscriptButtons && this._renderScrollToButton()}
501
522
  </div>
502
- {renderTranscriptButtons && this._renderScrollToButton()}
503
- </div>
504
- )}
505
- </div>
523
+ )}
524
+ </div>
525
+ </ScreenReaderProvider>
506
526
  );
507
527
  }
508
528
  }
@@ -8,7 +8,9 @@ export const TranscriptEvents = {
8
8
  TRANSCRIPT_POPOUT_OPEN: 'transcript_popout_open',
9
9
  TRANSCRIPT_POPOUT_CLOSE: 'transcript_popout_close',
10
10
  TRANSCRIPT_POPOUT_DRAG: 'transcript_popout_drag',
11
- TRANSCRIPT_POPOUT_RESIZE: 'transcript_popout_resize'
11
+ TRANSCRIPT_POPOUT_RESIZE: 'transcript_popout_resize',
12
+
13
+ TRANSCRIPT_TO_SEARCH_MATCH: 'transcript_to_search_match' // internal event
12
14
  };
13
15
 
14
16
  export enum CloseDetachTypes {
@@ -281,6 +281,10 @@ export class TranscriptPlugin extends KalturaPlayer.core.BasePlugin {
281
281
  return this.sidePanelsManager!.isItemDetached(this._transcriptPanel);
282
282
  };
283
283
 
284
+ private _toSearchMatch = () => {
285
+ this.dispatchEvent(TranscriptEvents.TRANSCRIPT_TO_SEARCH_MATCH);
286
+ };
287
+
284
288
  private _addTranscriptItem(): void {
285
289
  if (Math.max(this._transcriptPanel, this._transcriptIcon, this._audioPlayerIconId) > 0) {
286
290
  // transcript panel or icon already exist
@@ -330,6 +334,7 @@ export class TranscriptPlugin extends KalturaPlayer.core.BasePlugin {
330
334
  onAttach={() => {
331
335
  this._handleAttach(CloseDetachTypes.arrow);
332
336
  }}
337
+ onJumpToSearchMatch={this._toSearchMatch}
333
338
  />
334
339
  ) as any;
335
340
  },
@@ -22,7 +22,10 @@
22
22
  "attach_transcript": "Bring Transcript back",
23
23
  "detach_transcript": "Popout transcript",
24
24
  "transcript": "Transcript",
25
- "caption_label": "Jump to this point in video"
25
+ "caption_label": "Jump to this point in video",
26
+ "move_to_search": "Click to jump to search result",
27
+ "to_search_result": "Go to result",
28
+ "to_search_result_label": "Click to jump to this point in the video"
26
29
  }
27
30
  }
28
31
  }