@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/CHANGELOG.md +2 -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 +48 -28
- 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-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": "
|
|
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,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
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
</div>
|
|
523
|
+
)}
|
|
524
|
+
</div>
|
|
525
|
+
</ScreenReaderProvider>
|
|
506
526
|
);
|
|
507
527
|
}
|
|
508
528
|
}
|
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
|
}
|