media-transcript
Version:
A web component for an interactive transcript built from WebVTT cues.
241 lines (196 loc) • 4.35 kB
JavaScript
import {
hasAttributeToken,
timeStrToNumber,
} from './util';
/**
* The Transcript Cue is a container for the VTTCue text
* It functions like a control for the video location.
*/
let CueInterface = window.VTTCue;
class MediaCue extends HTMLElement {
constructor(cue = {}) {
super();
// VTTCue must be supported by the user agent or polyfilled by the user
if (!MediaCue.VTTCue) {
throw new Error(
'No VTTCue interface found. '
+ 'Please use a browser that supports VTTCue '
+ 'or supply a polyfill by setting MediaCue.VTTCue',
);
}
if (cue instanceof MediaCue.VTTCue) {
this.cue = cue;
} else {
const {
startTime = null,
endTime = null,
text = this.innerHTML || null,
} = cue;
this.cue = new MediaCue.VTTCue(startTime, endTime, text);
}
this.textSlot = document.createElement('slot');
/** create shadow root and add style + time + slot */
const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true });
shadowRoot.appendChild(this.textSlot);
this.textSlotchangeHandler = () => {
if (!this.updating) {
let text = '';
this.textSlot.assignedNodes().forEach(({ outerHTML, nodeValue }) => {
text += outerHTML || nodeValue;
});
this.cue.text = text;
// this.triggerSlot.innerHTML = text;
}
this.updating = false;
};
}
connectedCallback() {
this.selected = false;
}
attributeChangedCallback(attr, oldValue, newValue) {
switch (attr) {
case 'start':
case 'end':
if (newValue && !this.updating) {
// bind cue start/end to the attribute values
this.cue[`${attr}Time`] = timeStrToNumber(newValue);
}
break;
default:
}
}
update(cue) {
if (cue instanceof CueInterface) {
this.cue = cue;
} else {
this.cue.startTime = cue.startTime || this.startInSeconds();
this.cue.endTime = cue.endTime || this.endInSeconds();
this.cue.text = cue.text || this.text;
}
this.updating = true;
this.start = this.cue.startTime;
this.end = this.cue.endTime;
this.appendChild(this.cue.getCueAsHTML());
return this;
}
select() {
this.selected = true;
return this;
}
deselect() {
this.selected = false;
return this;
}
hasRole(...roles) {
return hasAttributeToken.call(this, 'role', ...roles);
}
/**
* @todo: parse the DOM nodes into valid WebVTT strings
* @see https://w3c.github.io/webvtt/#dom-construction-rules
*/
toVTTString() {
return this;
}
// PROPERTY REFLECTION
get ariaSelected() {
return this.getAttribute('aria-selected');
}
set ariaSelected(val) {
if (val !== null) {
this.setAttribute('aria-selected', val);
} else {
this.removeAttribute('aria-selected');
}
}
get role() {
return this.getAttribute('role');
}
set role(val) {
if (val) {
this.setAttribute('role', val);
} else {
this.removeAttribute('role');
}
}
get start() {
return this.getAttribute('start') || '0';
}
set start(val) {
if (val) {
this.setAttribute('start', val);
} else {
this.removeAttribute('start');
}
}
startInSeconds() {
return timeStrToNumber(this.start);
}
get end() {
return this.getAttribute('end');
}
set end(val) {
if (val) {
this.setAttribute('end', val);
} else {
this.removeAttribute('end');
}
}
endInSeconds() {
return timeStrToNumber(this.end);
}
get onclick() {
return this.clickHandler;
}
set onclick(fn) {
if (typeof fn === 'function') {
this.clickHandler = fn.bind(this);
this.addEventListener('click', this.clickHandler);
this.addEventListener('keydown', (e) => {
if (e.key === 'Enter') fn.call(this, e);
});
}
}
// PROPERTY ALIASES
get startTime() {
return this.cue.startTime;
}
set startTime(val) {
this.start = val;
}
get endTime() {
return this.cue.endTime;
}
set endTime(val) {
this.end = val;
}
get text() {
return this.innerHTML;
}
set text(val) {
this.innerHTML = val;
}
get selected() {
return this.ariaSelected;
}
set selected(v) {
this.ariaSelected = v;
}
get VTTCue() {
return this.cue;
}
// STATIC PROPERTIES & METHODS
static get VTTCue() {
return CueInterface;
}
static set VTTCue(val) {
CueInterface = val;
}
static get observedAttributes() {
return [
'start',
'end',
];
}
}
export default MediaCue;
customElements.define('media-cue', MediaCue);