@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/CHANGELOG.md +7 -0
- package/dist/playkit-transcript.js +1 -1
- package/dist/playkit-transcript.js.map +1 -1
- package/package.json +2 -2
- package/src/components/caption/caption.tsx +51 -15
- package/src/components/caption-list/captionList.tsx +28 -20
- package/src/components/transcript/transcript.scss +16 -2
- package/src/components/transcript/transcript.tsx +51 -27
- package/src/events/events.ts +3 -1
- package/src/transcript-plugin.tsx +5 -0
- package/translations/en.i18n.json +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playkit-js/transcript",
|
|
3
|
-
"version": "3.5.
|
|
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": "
|
|
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
|
|
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.
|
|
40
|
-
this.props.scrollTo(this.
|
|
41
|
-
} else if (this.
|
|
42
|
-
this.props.scrollToSearchMatch(this.
|
|
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
|
-
|
|
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} ${
|
|
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.
|
|
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
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
</div>
|
|
527
|
+
)}
|
|
528
|
+
</div>
|
|
529
|
+
</ScreenReaderProvider>
|
|
506
530
|
);
|
|
507
531
|
}
|
|
508
532
|
}
|
package/src/events/events.ts
CHANGED
|
@@ -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
|
}
|