@exadel/esl
Version:
Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components
486 lines (485 loc) • 16.7 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var ESLMedia_1;
import { ESLBaseElement } from '../../esl-base-element/core';
import { ExportNs } from '../../esl-utils/environment/export-ns';
import { isSafeContains } from '../../esl-utils/dom/traversing';
import { CSSClassUtils } from '../../esl-utils/dom/class';
import { SPACE, PAUSE } from '../../esl-utils/dom/keys';
import { isInViewport } from '../../esl-utils/dom/visible';
import { prop, attr, boolAttr, listen, memoize } from '../../esl-utils/decorators';
import { debounce } from '../../esl-utils/async';
import { parseAspectRatio, parseBoolean, parseLazyAttr } from '../../esl-utils/misc/format';
import { ESLMediaQuery } from '../../esl-media-query/core';
import { ESLResizeObserverTarget } from '../../esl-event-listener/core';
import { ESLTraversingQuery } from '../../esl-traversing-query/core';
import { getIObserver } from './esl-media-iobserver';
import { PlayerStates } from './esl-media-provider';
import { ESLMediaProviderRegistry } from './esl-media-registry';
import { ESLMediaManager } from './esl-media-manager';
import { ESLMediaHookEvent } from './esl-media.events';
/**
* ESLMedia - custom element, that provides an ability to add and configure media (video / audio)
* using a single tag as well as work with external providers using simple native-like API.
*
* @author Alexey Stsefanovich (ala'n), Yuliya Adamskaya
*/
let ESLMedia = ESLMedia_1 = class ESLMedia extends ESLBaseElement {
constructor() {
super(...arguments);
/** Deferred reinitialize handler, to prevent multiple reinitialization calls in bound of the macro-task */
this.deferredReinitialize = debounce(() => this.reinitInstance());
}
/** Singleton instance of {@link ESLMediaManager} */
static get manager() {
return new ESLMediaManager();
}
/**
* Map object with possible Player States, values:
* BUFFERING, ENDED, PAUSED, PLAYING, UNSTARTED, VIDEO_CUED, UNINITIALIZED
*/
static get PLAYER_STATES() {
return PlayerStates;
}
/** Returns true if the provider with given name is supported */
static supports(name) {
return ESLMediaProviderRegistry.instance.has(name);
}
/** @readonly {@link ESLMediaManager} used for current instance */
get manager() {
return this.constructor.manager;
}
connectedCallback() {
super.connectedCallback();
this.manager._onInit(this);
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'application');
}
this.innerHTML += '<!-- Inner Content, do not modify it manually -->';
this.deferredReinitialize();
this.reattachViewportConstraint();
}
disconnectedCallback() {
this.manager._onDestroy(this);
super.disconnectedCallback();
this.detachViewportConstraint();
this._provider && this._provider.unbind();
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (!this.connected || oldVal === newVal)
return;
switch (attrName) {
case 'media-id':
case 'media-src':
case 'media-type':
case 'start-time':
case 'lazy':
this.reattachViewportConstraint();
this.deferredReinitialize();
break;
case 'loop':
case 'muted':
case 'controls':
this._provider && this._provider.onSafeConfigChange(attrName, newVal !== null);
break;
case 'fill-mode':
case 'aspect-ratio':
this.$$on(this._onResize);
this._onResize();
break;
case 'play-in-viewport':
this.reattachViewportConstraint();
break;
case 'load-condition':
this.$$on(this._onConditionChange);
this.deferredReinitialize();
break;
}
}
canActivate() {
return this.lazy === 'none' && this.conditionQuery.matches;
}
reinitInstance() {
var _a;
(_a = this._provider) === null || _a === void 0 ? void 0 : _a.unbind();
this._provider = null;
this._isManualAction = false;
if (this.canActivate()) {
this._provider = ESLMediaProviderRegistry.instance.createFor(this);
if (!this._provider)
this._onError();
}
this.updateContainerMarkers();
}
updateContainerMarkers() {
const $target = ESLTraversingQuery.first(this.loadConditionClassTarget, this);
$target && CSSClassUtils.toggle($target, this.loadConditionClass, this.conditionQuery.matches);
}
/** Seek to given position of media */
seekTo(pos) {
var _a;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.safeSeekTo(pos)) || null;
}
/**
* Start playing media
* @param allowActivate - allows to remove manual lazy loading restrictions
* @param system - marks that the action was initiated by the system
*/
play(allowActivate = false, system = false) {
var _a;
if (!this.ready && allowActivate) {
this.lazy = 'none';
this.deferredReinitialize.cancel();
this.reinitInstance();
}
this._isManualAction = !system;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.safePlay(system)) || null;
}
/** Pause playing media */
pause(system = false) {
var _a;
this._isManualAction = !system;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.safePause()) || null;
}
/** Stop playing media */
stop(system = false) {
var _a;
this._isManualAction = !system;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.safeStop()) || null;
}
/**
* Executes toggle action:
* If the player is PAUSED then it starts playing otherwise it pause playing
*/
toggle(allowActivate = false) {
const shouldActivate = [PlayerStates.PAUSED, PlayerStates.UNSTARTED, PlayerStates.VIDEO_CUED, PlayerStates.UNINITIALIZED].includes(this.state);
return shouldActivate ? this.play(allowActivate) : this.pause();
}
/** Focus inner player **/
focusPlayer() {
var _a;
(_a = this._provider) === null || _a === void 0 ? void 0 : _a.focus();
}
/** Detects if the user manipulate trough native controls */
detectUserInteraction(cmd) {
var _a;
if (!this.controls || this._isManualAction)
return;
if (((_a = this._provider) === null || _a === void 0 ? void 0 : _a.lastCommand) === cmd)
return;
// User cannot manipulate the player outside the viewport
const tolerance = this.RATIO_TO_ACTIVATE * this.clientWidth * this.clientHeight;
if (!isInViewport(this, tolerance))
return;
console.debug('[ESL]: User %s interaction detected', cmd);
this._isManualAction = true;
}
// media live-cycle handlers
_onReady() {
this.$$attr('ready', true);
this.$$attr('error', false);
this.updateReadyClass();
this.$$fire(this.READY_EVENT);
this._onResize();
}
_onError(detail, setReadyState = true) {
this.$$attr('ready', true);
this.$$attr('error', true);
this.$$fire(this.ERROR_EVENT, { detail });
setReadyState && this.$$fire(this.READY_EVENT);
}
_onDetach() {
this.$$attr('active', false);
this.$$attr('ready', false);
this.$$attr('played', false);
this.updateReadyClass();
this.$$fire(this.DETACHED_EVENT);
}
_onBeforePlay(initiator) {
const event = new ESLMediaHookEvent(this.BEFORE_PLAY_EVENT, { initiator });
return this.dispatchEvent(event);
}
_onPlay() {
this.detectUserInteraction('play');
if (this.autofocus)
this.focus();
this.$$attr('active', true);
this.$$attr('played', true);
this.$$fire(this.PLAY_EVENT);
this.manager._onAfterPlay(this);
this._onResize();
}
_onPaused() {
this.detectUserInteraction('pause');
this.$$attr('active', false);
this.$$fire(this.PAUSED_EVENT);
}
_onEnded() {
this.detectUserInteraction('pause');
this.$$attr('active', false);
this.$$fire(this.ENDED_EVENT);
}
_onResize() {
if (!this._provider)
return;
const { actualAspectRatio } = this;
this.$$attr('wide', this.offsetWidth / this.offsetHeight > actualAspectRatio);
this._provider.setAspectRatio(actualAspectRatio);
}
_onRefresh(e) {
if (!isSafeContains(e.target, this))
return;
this._onResize();
}
_onRegistryStateChange(e) {
if (e.isRelates(this.mediaType))
this.reinitInstance();
}
_onConditionChange() {
this.deferredReinitialize();
}
_onKeydown(e) {
if (e.target !== this)
return;
if (![SPACE, PAUSE].includes(e.key))
return;
e.preventDefault();
e.stopPropagation();
this.toggle();
}
/** Update ready class state */
updateReadyClass() {
const target = ESLTraversingQuery.first(this.readyClassTarget, this);
target && CSSClassUtils.toggle(target, this.readyClass, this.ready);
}
/** Applied provider */
get providerType() {
var _a;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.name) || '';
}
/**
* Marker if the last action (play/pause/stop) was initiated by the user
* (direct method call or by embed player controls)
*/
get isUserInitiated() {
return this._isManualAction;
}
/** Current player state, see {@link ESLMedia.PLAYER_STATES} values */
get state() {
return this._provider ? this._provider.state : PlayerStates.UNINITIALIZED;
}
/** Duration of the media resource */
get duration() {
var _a;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.duration) || 0;
}
/** Current time of media resource */
get currentTime() {
var _a;
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.currentTime) || 0;
}
/** Set current time of media resource */
set currentTime(time) {
var _a;
(_a = this._provider) === null || _a === void 0 ? void 0 : _a.safeSeekTo(time);
}
/** ESLMediaQuery to limit ESLMedia loading */
get conditionQuery() {
return ESLMediaQuery.for(this.loadCondition);
}
/** Fill mode should be handled for element */
get fillModeEnabled() {
return this.fillMode === 'cover' || this.fillMode === 'inscribe';
}
/** Used resource aspect ratio forced by attribute or returned by provider */
get actualAspectRatio() {
var _a;
if (this.aspectRatio && this.aspectRatio !== 'auto')
return parseAspectRatio(this.aspectRatio);
return ((_a = this._provider) === null || _a === void 0 ? void 0 : _a.defaultAspectRatio) || 0;
}
reattachViewportConstraint() {
this.detachViewportConstraint();
if (!this.playInViewport && this.lazy !== 'auto')
return;
getIObserver().observe(this);
}
detachViewportConstraint() {
var _a;
(_a = getIObserver(true)) === null || _a === void 0 ? void 0 : _a.unobserve(this);
}
};
ESLMedia.is = 'esl-media';
ESLMedia.observedAttributes = [
'load-condition',
'media-type',
'media-id',
'media-src',
'fill-mode',
'aspect-ratio',
'play-in-viewport',
'muted',
'loop',
'controls',
'lazy',
'start-time'
];
__decorate([
prop(0.05)
], ESLMedia.prototype, "RATIO_TO_ACTIVATE", void 0);
__decorate([
prop(0.2)
], ESLMedia.prototype, "RATIO_TO_STOP", void 0);
__decorate([
prop(0.33)
], ESLMedia.prototype, "RATIO_TO_PLAY", void 0);
__decorate([
prop('esl:media:ready')
], ESLMedia.prototype, "READY_EVENT", void 0);
__decorate([
prop('esl:media:error')
], ESLMedia.prototype, "ERROR_EVENT", void 0);
__decorate([
prop('esl:media:before:play')
], ESLMedia.prototype, "BEFORE_PLAY_EVENT", void 0);
__decorate([
prop('esl:media:play')
], ESLMedia.prototype, "PLAY_EVENT", void 0);
__decorate([
prop('esl:media:paused')
], ESLMedia.prototype, "PAUSED_EVENT", void 0);
__decorate([
prop('esl:media:ended')
], ESLMedia.prototype, "ENDED_EVENT", void 0);
__decorate([
prop('esl:media:detached')
], ESLMedia.prototype, "DETACHED_EVENT", void 0);
__decorate([
prop('esl:media:managedpause')
], ESLMedia.prototype, "MANAGED_PAUSE_EVENT", void 0);
__decorate([
attr()
], ESLMedia.prototype, "mediaId", void 0);
__decorate([
attr()
], ESLMedia.prototype, "mediaSrc", void 0);
__decorate([
attr()
], ESLMedia.prototype, "mediaType", void 0);
__decorate([
attr()
], ESLMedia.prototype, "group", void 0);
__decorate([
attr()
], ESLMedia.prototype, "fillMode", void 0);
__decorate([
attr()
], ESLMedia.prototype, "aspectRatio", void 0);
__decorate([
attr({ parser: parseLazyAttr })
], ESLMedia.prototype, "lazy", void 0);
__decorate([
boolAttr()
], ESLMedia.prototype, "autoplay", void 0);
__decorate([
boolAttr()
], ESLMedia.prototype, "autofocus", void 0);
__decorate([
boolAttr()
], ESLMedia.prototype, "muted", void 0);
__decorate([
boolAttr()
], ESLMedia.prototype, "loop", void 0);
__decorate([
boolAttr()
], ESLMedia.prototype, "controls", void 0);
__decorate([
boolAttr({ name: 'playsinline' })
], ESLMedia.prototype, "playsInline", void 0);
__decorate([
boolAttr({ name: 'disablepictureinpicture' })
], ESLMedia.prototype, "disablePictureInPicture", void 0);
__decorate([
boolAttr()
], ESLMedia.prototype, "playInViewport", void 0);
__decorate([
attr({ defaultValue: 0, parser: parseInt })
], ESLMedia.prototype, "startTime", void 0);
__decorate([
attr({
parser: parseBoolean,
defaultValue: ($this) => $this.controls
})
], ESLMedia.prototype, "focusable", void 0);
__decorate([
attr({ defaultValue: 'auto' })
], ESLMedia.prototype, "preload", void 0);
__decorate([
attr()
], ESLMedia.prototype, "readyClass", void 0);
__decorate([
attr()
], ESLMedia.prototype, "readyClassTarget", void 0);
__decorate([
attr({ defaultValue: 'all' })
], ESLMedia.prototype, "loadCondition", void 0);
__decorate([
attr()
], ESLMedia.prototype, "loadConditionClass", void 0);
__decorate([
attr({ defaultValue: '::parent' })
], ESLMedia.prototype, "loadConditionClassTarget", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLMedia.prototype, "ready", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLMedia.prototype, "active", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLMedia.prototype, "played", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLMedia.prototype, "error", void 0);
__decorate([
boolAttr({ readonly: true })
], ESLMedia.prototype, "wide", void 0);
__decorate([
listen({
event: 'resize',
target: ESLResizeObserverTarget.for,
condition: ($this) => $this.fillModeEnabled
})
], ESLMedia.prototype, "_onResize", null);
__decorate([
listen({
event: ($this) => $this.REFRESH_EVENT,
target: window
})
], ESLMedia.prototype, "_onRefresh", null);
__decorate([
listen({
event: 'change',
target: () => ESLMediaProviderRegistry.instance
})
], ESLMedia.prototype, "_onRegistryStateChange", null);
__decorate([
listen({
event: 'change',
target: ($this) => $this.conditionQuery
})
], ESLMedia.prototype, "_onConditionChange", null);
__decorate([
listen('keydown')
], ESLMedia.prototype, "_onKeydown", null);
__decorate([
memoize()
], ESLMedia, "manager", null);
ESLMedia = ESLMedia_1 = __decorate([
ExportNs('Media')
], ESLMedia);
export { ESLMedia };