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

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-7fa5f63",
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,8 @@ 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>
32
34
  };
33
35
 
34
36
  export interface TranscriptProps {
@@ -53,6 +55,7 @@ export interface TranscriptProps {
53
55
  errorDescripton?: string;
54
56
  attachTranscript?: string;
55
57
  detachTranscript?: string;
58
+ toSearchResult?: string;
56
59
  downloadDisabled: boolean;
57
60
  onDownload: () => void;
58
61
  printDisabled: boolean;
@@ -66,6 +69,7 @@ export interface TranscriptProps {
66
69
  kitchenSinkDetached: boolean;
67
70
  isMobile?: boolean;
68
71
  playerWidth?: number;
72
+ onJumpToSearchMatch: () => void;
69
73
  }
70
74
 
71
75
  interface TranscriptState {
@@ -268,6 +272,24 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
268
272
  return styles.smallWidth;
269
273
  };
270
274
 
275
+ private _renderJumpToSearchButton = () => {
276
+ const {toSearchResult, onJumpToSearchMatch} = this.props;
277
+ const {search, activeSearchIndex, totalSearchResults} = this.state;
278
+ if (!search || totalSearchResults === 0 || activeSearchIndex === 0) {
279
+ return null;
280
+ }
281
+ return (
282
+ <Button
283
+ type={ButtonType.secondary}
284
+ className={styles.toSearchButton}
285
+ onClick={onJumpToSearchMatch}
286
+ ariaLabel={toSearchResult}
287
+ testId="transcript_jumpToSearchMatch">
288
+ {toSearchResult}
289
+ </Button>
290
+ );
291
+ };
292
+
271
293
  private _renderHeader = () => {
272
294
  const {
273
295
  toggledWithEnter,
@@ -294,7 +316,6 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
294
316
  disabled: isLoading
295
317
  };
296
318
  }
297
-
298
319
  return (
299
320
  <div className={[styles.header, this._getHeaderStyles()].join(' ')} data-testid="transcript_header">
300
321
  <Search
@@ -306,6 +327,7 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
306
327
  toggledWithEnter={toggledWithEnter}
307
328
  kitchenSinkActive={kitchenSinkActive}
308
329
  />
330
+ {this._renderJumpToSearchButton()}
309
331
  <TranscriptMenu {...{downloadDisabled, onDownload, printDisabled, onPrint, isLoading, detachMenuItem, kitchenSinkDetached}} />
310
332
  {!detachMenuItem && this._renderDetachButton()}
311
333
  {!kitchenSinkDetached && (
@@ -477,32 +499,34 @@ export class Transcript extends Component<TranscriptProps, TranscriptState> {
477
499
  const {isLoading, kitchenSinkActive, kitchenSinkDetached, hasError, smallScreen, toggledWithEnter} = props;
478
500
  const renderTranscriptButtons = !(isLoading || hasError);
479
501
  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()}
502
+ <ScreenReaderProvider>
503
+ <div
504
+ className={`${styles.root} ${kitchenSinkActive || kitchenSinkDetached ? '' : styles.hidden}`}
505
+ ref={node => {
506
+ this._widgetRootRef = node;
507
+ }}
508
+ data-testid="transcript_root">
509
+ {smallScreen && !kitchenSinkDetached ? (
510
+ <SmallScreenSlate onClose={this.props.onClose} toggledWithEnter={toggledWithEnter} />
511
+ ) : (
512
+ <div className={styles.globalContainer}>
513
+ {this._renderHeader()}
514
+
515
+ {renderTranscriptButtons && this._renderSkipTranscriptButton()}
516
+ <div
517
+ className={styles.body}
518
+ onScroll={this._onScroll}
519
+ ref={node => {
520
+ this._transcriptListRef = node;
521
+ }}
522
+ data-testid="transcript_list">
523
+ {isLoading ? this._renderLoading() : this._renderTranscript()}
524
+ </div>
525
+ {renderTranscriptButtons && this._renderScrollToButton()}
501
526
  </div>
502
- {renderTranscriptButtons && this._renderScrollToButton()}
503
- </div>
504
- )}
505
- </div>
527
+ )}
528
+ </div>
529
+ </ScreenReaderProvider>
506
530
  );
507
531
  }
508
532
  }
@@ -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
  }