playable
Version:
Video player based on HTML5Video
439 lines (365 loc) • 11.8 kB
text/typescript
import { TextLabel } from '../../../../constants';
import View from '../../core/view';
import { IView } from '../../core/types';
import { ITooltipReference, ITooltipService } from '../../core/tooltip/types';
import getProgressTimeTooltipPosition from './utils/getProgressTimeTooltipPosition';
import { progressTemplate, progressTimeIndicatorTemplate } from './templates';
import htmlToElement from '../../core/htmlToElement';
import getElementByHook from '../../core/getElementByHook';
import toggleElementClass from '../../core/toggleElementClass';
import {
IProgressViewStyles,
IProgressViewCallbacks,
IProgressViewConfig,
IProgressDragEvents,
} from './types';
import { ITextMap } from '../../../text-map/types';
import progressViewTheme from './progress.theme';
import styles from './progress.scss';
const DATA_PLAYED = 'data-playable-played-percent';
const getPercentBasedOnXPosition = (
event: MouseEvent,
element: HTMLElement,
) => {
const boundingRect = element.getBoundingClientRect();
const positionX = event.clientX;
if (positionX < boundingRect.left) {
return 0;
}
if (positionX > boundingRect.left + boundingRect.width) {
return 100;
}
return ((event.clientX - boundingRect.left) / boundingRect.width) * 100;
};
const getSupportedDragEventNames = (): IProgressDragEvents => {
if ('onpointerdown' in window) {
return {
mouseDown: 'pointerdown',
mouseMove: 'pointermove',
mouseOut: 'pointerout',
mouseUp: 'pointerup',
};
}
if ('ontouchstart' in window) {
return {
mouseDown: 'touchstart',
mouseMove: 'touchmove',
mouseOut: 'mouseout',
mouseUp: 'touchend',
};
}
return {
mouseDown: 'mousedown',
mouseMove: 'mousemove',
mouseOut: 'mouseout',
mouseUp: 'mouseup',
};
};
class ProgressView extends View<IProgressViewStyles>
implements IView<IProgressViewStyles> {
private _callbacks: IProgressViewCallbacks;
private _textMap: ITextMap;
private _tooltipService: ITooltipService;
private _syncButtonTooltipReference: ITooltipReference;
private _isDragging: boolean;
private _currentPlayedPercent: number;
private _dragEvents: IProgressDragEvents;
private _$rootElement: HTMLElement;
private _$hitbox: HTMLElement;
private _$played: HTMLElement;
private _$buffered: HTMLElement;
private _$seekTo: HTMLElement;
private _$timeIndicators: HTMLElement;
private _$seekButton: HTMLElement;
private _$syncButton: HTMLElement;
constructor(config: IProgressViewConfig) {
const { callbacks, textMap, tooltipService, theme } = config;
super(theme);
this._callbacks = callbacks;
this._textMap = textMap;
this._tooltipService = tooltipService;
this._dragEvents = getSupportedDragEventNames();
this._initDOM();
this._bindCallbacks();
this._bindEvents();
this._setPlayedDOMAttributes(0);
this._setBufferedDOMAttributes(0);
this.setUsualMode();
}
private _initDOM() {
this._$rootElement = htmlToElement(
progressTemplate({
styles: this.styleNames,
themeStyles: this.themeStyles,
}),
);
this._$played = getElementByHook(this._$rootElement, 'progress-played');
this._$buffered = getElementByHook(this._$rootElement, 'progress-buffered');
this._$seekTo = getElementByHook(this._$rootElement, 'progress-seek-to');
this._$timeIndicators = getElementByHook(
this._$rootElement,
'progress-time-indicators',
);
this._$seekButton = getElementByHook(
this._$rootElement,
'progress-seek-button',
);
this._$syncButton = getElementByHook(
this._$rootElement,
'progress-sync-button',
);
this._syncButtonTooltipReference = this._tooltipService.createReference(
this._$syncButton,
{
text: this._textMap.get(TextLabel.LIVE_SYNC_TOOLTIP),
},
);
this._$hitbox = getElementByHook(this._$rootElement, 'progress-hitbox');
}
private _bindCallbacks() {
this._setPlayedByDrag = this._setPlayedByDrag.bind(this);
this._startDragOnMouseDown = this._startDragOnMouseDown.bind(this);
this._stopDragOnMouseUp = this._stopDragOnMouseUp.bind(this);
this._startSeekToByMouse = this._startSeekToByMouse.bind(this);
this._stopSeekToByMouse = this._stopSeekToByMouse.bind(this);
this._syncWithLive = this._syncWithLive.bind(this);
}
private _bindEvents() {
this._$seekButton.addEventListener(
this._dragEvents.mouseDown,
this._startDragOnMouseDown,
);
this._$seekButton.addEventListener(
this._dragEvents.mouseMove,
this._startSeekToByMouse,
);
this._$seekButton.addEventListener(
this._dragEvents.mouseOut,
this._stopSeekToByMouse,
);
this._$hitbox.addEventListener(
this._dragEvents.mouseDown,
this._startDragOnMouseDown,
);
this._$hitbox.addEventListener(
this._dragEvents.mouseMove,
this._startSeekToByMouse,
);
this._$hitbox.addEventListener(
this._dragEvents.mouseOut,
this._stopSeekToByMouse,
);
window.addEventListener(this._dragEvents.mouseMove, this._setPlayedByDrag);
window.addEventListener(this._dragEvents.mouseUp, this._stopDragOnMouseUp);
this._$syncButton.addEventListener('click', this._syncWithLive);
this._$syncButton.addEventListener(
'mouseenter',
this._callbacks.onSyncWithLiveMouseEnter,
);
this._$syncButton.addEventListener(
'mouseleave',
this._callbacks.onSyncWithLiveMouseLeave,
);
}
private _unbindEvents() {
this._$seekButton.removeEventListener(
this._dragEvents.mouseDown,
this._startDragOnMouseDown,
);
this._$seekButton.removeEventListener(
this._dragEvents.mouseMove,
this._startSeekToByMouse,
);
this._$seekButton.removeEventListener(
this._dragEvents.mouseOut,
this._stopSeekToByMouse,
);
this._$hitbox.removeEventListener(
this._dragEvents.mouseDown,
this._startDragOnMouseDown,
);
this._$hitbox.removeEventListener(
this._dragEvents.mouseMove,
this._startSeekToByMouse,
);
this._$hitbox.removeEventListener(
this._dragEvents.mouseOut,
this._stopSeekToByMouse,
);
window.removeEventListener(
this._dragEvents.mouseMove,
this._setPlayedByDrag,
);
window.removeEventListener(
this._dragEvents.mouseUp,
this._stopDragOnMouseUp,
);
this._$syncButton.removeEventListener('click', this._syncWithLive);
this._$syncButton.removeEventListener(
'mouseenter',
this._callbacks.onSyncWithLiveMouseEnter,
);
this._$syncButton.removeEventListener(
'mouseleave',
this._callbacks.onSyncWithLiveMouseLeave,
);
}
private _startDragOnMouseDown(event: MouseEvent) {
if (event.button > 1) {
return;
}
const percent = getPercentBasedOnXPosition(event, this._$hitbox);
this._setPlayedDOMAttributes(percent);
this._callbacks.onChangePlayedPercent(percent);
this._startDrag();
}
private _stopDragOnMouseUp(event: MouseEvent) {
if (event.button > 1) {
return;
}
this._stopDrag();
}
private _startSeekToByMouse(event: MouseEvent) {
const percent = getPercentBasedOnXPosition(event, this._$hitbox);
this._setSeekToDOMAttributes(percent);
this._callbacks.onSeekToByMouseStart(percent);
}
private _stopSeekToByMouse() {
this._setSeekToDOMAttributes(0);
this._callbacks.onSeekToByMouseEnd();
}
private _setPlayedByDrag(event: MouseEvent) {
if (this._isDragging) {
const percent = getPercentBasedOnXPosition(event, this._$hitbox);
this._setPlayedDOMAttributes(percent);
this._callbacks.onChangePlayedPercent(percent);
}
}
private _startDrag() {
this._isDragging = true;
this._callbacks.onDragStart();
this._$rootElement.classList.add(this.styleNames.isDragging);
}
private _stopDrag() {
if (this._isDragging) {
this._isDragging = false;
this._callbacks.onDragEnd();
this._$rootElement.classList.remove(this.styleNames.isDragging);
}
}
private _setSeekToDOMAttributes(percent: number) {
this._$seekTo.setAttribute('style', `width:${percent}%;`);
}
private _setPlayedDOMAttributes(percent: number) {
this._$rootElement.setAttribute(
'aria-valuetext',
this._textMap.get(TextLabel.PROGRESS_CONTROL_VALUE, { percent }),
);
this._$rootElement.setAttribute('aria-valuenow', percent.toFixed(2));
this._$rootElement.setAttribute(DATA_PLAYED, percent.toFixed(2));
this._setPlayedDOMPosition(percent);
}
private _setPlayedDOMPosition(percent: number) {
const scaleValue = percent / 100;
const translateValue =
this._$rootElement.getBoundingClientRect().width * scaleValue;
this._$played.style.transform = `scaleX(${scaleValue.toFixed(3)})`;
this._$seekButton.style.transform = `translateX(${translateValue.toFixed(
3,
)}px)`;
}
private _setBufferedDOMAttributes(percent: number) {
this._$buffered.setAttribute('style', `width:${percent}%;`);
}
private _syncWithLive() {
this._callbacks.onSyncWithLiveClick();
}
updateOnResize() {
this._setPlayedDOMPosition(this._currentPlayedPercent);
}
showSyncWithLive() {
this._$syncButton.classList.remove(this.styleNames.hidden);
}
hideSyncWithLive() {
this._$syncButton.classList.add(this.styleNames.hidden);
}
setLiveSyncState(isSync: boolean) {
toggleElementClass(this._$syncButton, this.styleNames.liveSync, isSync);
toggleElementClass(this._$seekButton, this.styleNames.liveSync, isSync);
if (isSync) {
this._syncButtonTooltipReference.disable();
this._$played.setAttribute('style', `width:100%;`);
} else {
this._syncButtonTooltipReference.enable();
}
}
showProgressTimeTooltip(element: HTMLElement, percent: number) {
this._tooltipService.show({
element,
position: tooltipContainer =>
getProgressTimeTooltipPosition(
percent,
this._$hitbox,
tooltipContainer,
),
});
}
hideProgressTimeTooltip() {
this._tooltipService.hide();
}
setLiveMode() {
this._$rootElement.classList.add(this.styleNames.inLive);
this.showSyncWithLive();
}
setUsualMode() {
this._$rootElement.classList.remove(this.styleNames.inLive);
this.hideSyncWithLive();
}
setPlayed(percent: number) {
this._currentPlayedPercent = percent;
this._setPlayedDOMAttributes(percent);
}
setBuffered(percent: number) {
this._setBufferedDOMAttributes(percent);
}
addTimeIndicator(percent: number) {
this._$timeIndicators.appendChild(
htmlToElement(
progressTimeIndicatorTemplate({
percent,
styles: this.styleNames,
}),
),
);
}
clearTimeIndicators() {
this._$timeIndicators.innerHTML = '';
}
hide() {
this._$rootElement.classList.add(this.styleNames.hidden);
}
show() {
this._$rootElement.classList.remove(this.styleNames.hidden);
}
getElement() {
return this._$rootElement;
}
destroy() {
this._unbindEvents();
this._syncButtonTooltipReference.destroy();
if (this._$rootElement.parentNode) {
this._$rootElement.parentNode.removeChild(this._$rootElement);
}
this._$rootElement = null;
this._$buffered = null;
this._$hitbox = null;
this._$played = null;
this._$seekTo = null;
this._$seekButton = null;
this._$syncButton = null;
this._$timeIndicators = null;
}
}
ProgressView.setTheme(progressViewTheme);
ProgressView.extendStyleNames(styles);
export default ProgressView;