UNPKG

media-chrome

Version:

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

196 lines (166 loc) 5.42 kB
import { globalThis } from '../utils/server-safe-globals.js'; import { MediaUIAttributes, MediaUIEvents } from '../constants.js'; import { getMediaController, getStringAttr, setStringAttr, getNumericAttr, setNumericAttr, } from '../utils/element-utils.js'; import { parseRenditionList } from '../utils/utils.js'; import { MediaChromeMenu, createMenuItem, createIndicator, } from './media-chrome-menu.js'; import { Rendition } from '../media-store/state-mediator.js'; /** * @extends {MediaChromeMenu} * * @slot - Default slotted elements. * @slot header - An element shown at the top of the menu. * @slot checked-indicator - An icon element indicating a checked menu-item. * * @attr {string} mediarenditionselected - (read-only) Set to the selected rendition id. * @attr {string} mediarenditionlist - (read-only) Set to the rendition list. */ class MediaRenditionMenu extends MediaChromeMenu { static get observedAttributes(): string[] { return [ ...super.observedAttributes, MediaUIAttributes.MEDIA_RENDITION_LIST, MediaUIAttributes.MEDIA_RENDITION_SELECTED, MediaUIAttributes.MEDIA_RENDITION_UNAVAILABLE, MediaUIAttributes.MEDIA_HEIGHT, ]; } #renditionList: Rendition[] = []; #prevState: Record<string, any> = {}; attributeChangedCallback( attrName: string, oldValue: string | null, newValue: string | null ): void { super.attributeChangedCallback(attrName, oldValue, newValue); if ( attrName === MediaUIAttributes.MEDIA_RENDITION_SELECTED && oldValue !== newValue ) { this.value = newValue ?? 'auto'; this.#render(); } else if ( attrName === MediaUIAttributes.MEDIA_RENDITION_LIST && oldValue !== newValue ) { this.#renditionList = parseRenditionList(newValue); this.#render(); } else if ( attrName === MediaUIAttributes.MEDIA_HEIGHT && oldValue !== newValue ) { this.#render(); } } connectedCallback(): void { super.connectedCallback(); this.addEventListener('change', this.#onChange); } disconnectedCallback(): void { super.disconnectedCallback(); this.removeEventListener('change', this.#onChange); } /** * Returns the anchor element when it is a floating menu. */ get anchorElement() { if (this.anchor !== 'auto') return super.anchorElement; return getMediaController(this).querySelector<HTMLElement>( 'media-rendition-menu-button' ); } get mediaRenditionList(): Rendition[] { return this.#renditionList; } set mediaRenditionList(list: Rendition[]) { this.#renditionList = list; this.#render(); } /** * Get selected rendition id. */ get mediaRenditionSelected(): string { return getStringAttr(this, MediaUIAttributes.MEDIA_RENDITION_SELECTED); } set mediaRenditionSelected(id: string) { setStringAttr(this, MediaUIAttributes.MEDIA_RENDITION_SELECTED, id); } get mediaHeight(): number { return getNumericAttr(this, MediaUIAttributes.MEDIA_HEIGHT); } set mediaHeight(height: number) { setNumericAttr(this, MediaUIAttributes.MEDIA_HEIGHT, height); } #render(): void { if ( this.#prevState.mediaRenditionList === JSON.stringify(this.mediaRenditionList) && this.#prevState.mediaHeight === this.mediaHeight ) return; this.#prevState.mediaRenditionList = JSON.stringify(this.mediaRenditionList); this.#prevState.mediaHeight = this.mediaHeight; const renditionList = this.mediaRenditionList.sort( (a: any, b: any) => b.height - a.height ); for (const rendition of renditionList) { // `selected` is not serialized in the rendition list because // each selection would cause a re-render of the menu. // @ts-ignore rendition.selected = rendition.id === this.mediaRenditionSelected; } this.defaultSlot.textContent = ''; const isAuto = !this.mediaRenditionSelected; for (const rendition of renditionList) { const text = this.formatMenuItemText( `${Math.min(rendition.width as number, rendition.height as number)}p`, rendition ); const item = createMenuItem({ type: 'radio', text, value: `${rendition.id}`, checked: rendition.selected && !isAuto, }); item.prepend(createIndicator(this, 'checked-indicator')); this.defaultSlot.append(item); } const text = isAuto ? this.formatMenuItemText(`Auto (${this.mediaHeight}p)`) : this.formatMenuItemText('Auto'); const item = createMenuItem({ type: 'radio', text, value: 'auto', checked: isAuto, }); const autoDescription = this.mediaHeight > 0 ? `Auto (${this.mediaHeight}p)` : 'Auto'; item.dataset.description = autoDescription; item.prepend(createIndicator(this, 'checked-indicator')); this.defaultSlot.append(item); } #onChange(): void { if (this.value == null) return; const event = new globalThis.CustomEvent( MediaUIEvents.MEDIA_RENDITION_REQUEST, { composed: true, bubbles: true, detail: this.value, } ); this.dispatchEvent(event); } } if (!globalThis.customElements.get('media-rendition-menu')) { globalThis.customElements.define('media-rendition-menu', MediaRenditionMenu); } export { MediaRenditionMenu }; export default MediaRenditionMenu;