UNPKG

media-chrome

Version:

Custom elements (web components) for making audio and video player controls that look great in your website or app.

245 lines (212 loc) • 7.72 kB
import { MediaUIAttributes, MediaStateReceiverAttributes, } from './constants.js'; import { globalThis } from './utils/server-safe-globals.js'; import { getBooleanAttr, setBooleanAttr, getOrInsertCSSRule, setStringAttr, getStringAttr, namedNodeMapToObject, } from './utils/element-utils.js'; import MediaController from './media-controller.js'; import { t } from './utils/i18n.js'; export const Attributes = { LOADING_DELAY: 'loadingdelay', NO_AUTOHIDE: 'noautohide', }; const DEFAULT_LOADING_DELAY = 500; const loadingIndicatorIcon = ` <svg aria-hidden="true" viewBox="0 0 100 100"> <path d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"> <animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="1s" from="0 50 50" to="360 50 50" repeatCount="indefinite" /> </path> </svg> `; function getTemplateHTML(_attrs: Record<string, string>) { return /*html*/ ` <style> :host { display: var(--media-control-display, var(--media-loading-indicator-display, inline-block)); vertical-align: middle; box-sizing: border-box; --_loading-indicator-delay: var(--media-loading-indicator-transition-delay, ${DEFAULT_LOADING_DELAY}ms); } #status { color: rgba(0,0,0,0); width: 0px; height: 0px; } :host slot[name=icon] > *, :host ::slotted([slot=icon]) { opacity: var(--media-loading-indicator-opacity, 0); transition: opacity 0.15s; } :host([${MediaUIAttributes.MEDIA_LOADING}]:not([${ MediaUIAttributes.MEDIA_PAUSED }])) slot[name=icon] > *, :host([${MediaUIAttributes.MEDIA_LOADING}]:not([${ MediaUIAttributes.MEDIA_PAUSED }])) ::slotted([slot=icon]) { opacity: var(--media-loading-indicator-opacity, 1); transition: opacity 0.15s var(--_loading-indicator-delay); } :host #status { visibility: var(--media-loading-indicator-opacity, hidden); transition: visibility 0.15s; } :host([${MediaUIAttributes.MEDIA_LOADING}]:not([${ MediaUIAttributes.MEDIA_PAUSED }])) #status { visibility: var(--media-loading-indicator-opacity, visible); transition: visibility 0.15s var(--_loading-indicator-delay); } svg, img, ::slotted(svg), ::slotted(img) { width: var(--media-loading-indicator-icon-width); height: var(--media-loading-indicator-icon-height, 100px); fill: var(--media-icon-color, var(--media-primary-color, rgb(238 238 238))); vertical-align: middle; } </style> <slot name="icon">${loadingIndicatorIcon}</slot> <div id="status" role="status" aria-live="polite">${t('media loading')}</div> `; } /** * @slot icon - The element shown for when the media is in a buffering state. * * @attr {string} loadingdelay - Set the delay in ms before the loading animation is shown. * @attr {string} mediacontroller - The element `id` of the media controller to connect to (if not nested within). * @attr {boolean} mediapaused - (read-only) Present if the media is paused. * @attr {boolean} medialoading - (read-only) Present if the media is loading. * * @cssproperty --media-primary-color - Default color of text and icon. * @cssproperty --media-icon-color - `fill` color of button icon. * * @cssproperty --media-control-display - `display` property of control. * * @cssproperty --media-loading-indicator-display - `display` property of loading indicator. * @cssproperty [ --media-loading-indicator-opacity = 0 ] - `opacity` property of loading indicator. Set to 1 to force it to be visible. * @cssproperty [ --media-loading-indicator-transition-delay = 500ms ] - `transition-delay` property of loading indicator. Make sure to include units. * @cssproperty --media-loading-indicator-icon-width - `width` of loading icon. * @cssproperty [ --media-loading-indicator-icon-height = 100px ] - `height` of loading icon. */ class MediaLoadingIndicator extends globalThis.HTMLElement { static shadowRootOptions = { mode: 'open' as ShadowRootMode }; static getTemplateHTML = getTemplateHTML; #mediaController: MediaController; #delay = DEFAULT_LOADING_DELAY; static get observedAttributes(): string[] { return [ MediaStateReceiverAttributes.MEDIA_CONTROLLER, MediaUIAttributes.MEDIA_PAUSED, MediaUIAttributes.MEDIA_LOADING, Attributes.LOADING_DELAY, ]; } constructor() { super(); if (!this.shadowRoot) { // Set up the Shadow DOM if not using Declarative Shadow DOM. this.attachShadow((this.constructor as typeof MediaLoadingIndicator).shadowRootOptions); const attrs = namedNodeMapToObject(this.attributes); this.shadowRoot.innerHTML = (this.constructor as typeof MediaLoadingIndicator).getTemplateHTML(attrs); } } attributeChangedCallback( attrName: string, oldValue: string | null, newValue: string | null ): void { if (attrName === Attributes.LOADING_DELAY && oldValue !== newValue) { this.loadingDelay = Number(newValue); } else if (attrName === MediaStateReceiverAttributes.MEDIA_CONTROLLER) { if (oldValue) { this.#mediaController?.unassociateElement?.(this); this.#mediaController = null; } if (newValue && this.isConnected) { // @ts-ignore this.#mediaController = this.getRootNode()?.getElementById(newValue); this.#mediaController?.associateElement?.(this); } } } connectedCallback(): void { const mediaControllerId = this.getAttribute( MediaStateReceiverAttributes.MEDIA_CONTROLLER ); if (mediaControllerId) { // @ts-ignore this.#mediaController = (this.getRootNode() as Document)?.getElementById( mediaControllerId ); this.#mediaController?.associateElement?.(this); } } disconnectedCallback(): void { // Use cached mediaController, getRootNode() doesn't work if disconnected. this.#mediaController?.unassociateElement?.(this); this.#mediaController = null; } /** * Delay in ms */ get loadingDelay(): number { return this.#delay; } set loadingDelay(delay: number) { this.#delay = delay; const { style } = getOrInsertCSSRule(this.shadowRoot, ':host'); style.setProperty( '--_loading-indicator-delay', `var(--media-loading-indicator-transition-delay, ${delay}ms)` ); } /** * Is the media paused */ get mediaPaused(): boolean { return getBooleanAttr(this, MediaUIAttributes.MEDIA_PAUSED); } set mediaPaused(value: boolean) { setBooleanAttr(this, MediaUIAttributes.MEDIA_PAUSED, value); } /** * Is the media loading */ get mediaLoading(): boolean { return getBooleanAttr(this, MediaUIAttributes.MEDIA_LOADING); } set mediaLoading(value: boolean) { setBooleanAttr(this, MediaUIAttributes.MEDIA_LOADING, value); } get mediaController(): string | undefined { return getStringAttr(this, MediaStateReceiverAttributes.MEDIA_CONTROLLER); } set mediaController(value: string | undefined) { setStringAttr(this, MediaStateReceiverAttributes.MEDIA_CONTROLLER, value); } get noAutohide(): boolean | undefined { return getBooleanAttr(this, Attributes.NO_AUTOHIDE); } set noAutohide(value: boolean | undefined) { setBooleanAttr(this, Attributes.NO_AUTOHIDE, value); } } if (!globalThis.customElements.get('media-loading-indicator')) { globalThis.customElements.define( 'media-loading-indicator', MediaLoadingIndicator ); } export default MediaLoadingIndicator;