UNPKG

media-show

Version:

Showcase your media player in style!

167 lines (143 loc) 4.05 kB
import { MsBaseElement } from './base-element.js'; import { render, html, css, unsafeStatic, templateInstance } from './html.js'; import { round, kebabCase } from './utils.js'; const styles = (props) => css` * { box-sizing: border-box; } :host { --ms-padding-internal: var(--ms-padding, 0.5rem); display: inline-block; line-height: 0; } ::slotted([slot='media']) { width: 100%; } ${props.localName}, video { width: 100%; } ${props.sizes .map( (size) => css` .size-${CSS.escape(size)} { float: left; ${props.aspectRatio < 1 ? `width: calc(${size} * ${props.aspectRatio});` : `width: ${size};`} padding: var(--ms-padding-internal); } ` ) .join('')} `; const defaultMediaTemplate = document.createElement('template'); defaultMediaTemplate.innerHTML = ` <video slot="media" src="{{src}}" poster="{{poster}}" controls="{{controls}}" preload="{{preload}}" autoplay="{{autoplay}}" muted="{{muted}}" loop="{{loop}}" ></video> `; const template = (props) => { // prettier-ignore return html` <style> ${styles(props)} </style> <div class="size-${props.sizes[0]}"> <slot name="media"></slot> <slot></slot> </div> ${unsafeStatic(...props.sizes.slice(1).map((size, i) => html` <div class="size-${size}"> ${templateInstance(props.template, props, { key: i })} </div> `))}`; }; // Warning: assuming there is just 1 instance of media-show. const mediaTemplate = ( document.querySelector('media-show template') ?? defaultMediaTemplate )?.content.cloneNode(true); const includedAttrs = Array.from( mediaTemplate?.querySelector('[slot=media]').attributes ?? [] ) .map(({ value }) => { const propMatches = value.match(/\{\{\s*(\w*?)\s*\}\}/); return propMatches && propMatches[1]; }) .filter(Boolean) .map((prop) => `media-${kebabCase(prop)}`); class MsSizes extends MsBaseElement { static observedAttributes = ['media-show', 'sizes', ...includedAttrs]; constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.addEventListener('slotchange', () => { this.aspectRatio = round( this.media?.offsetWidth / this.media?.offsetHeight ); const resizeObserver = new ResizeObserver(() => { const ratio = round(this.media?.offsetWidth / this.media?.offsetHeight); if (this.aspectRatio !== ratio) { this.aspectRatio = ratio; this.render(); } }); resizeObserver.observe(this.media); }); } attributeChangedCallback() { this.render(); } get template() { return this.querySelector('template') ?? defaultMediaTemplate; } get templateAttrNodes() { return Array.from( this.template?.content.querySelector('[slot=media]').attributes ?? [] ); } get media() { return this.querySelector('[slot=media]'); } render() { // Get all the props that were used in the template. const templateProps = this.templateAttrNodes .map(({ value }) => { const propMatches = value.match(/\{\{\s*(\w*?)\s*\}\}/); return propMatches && propMatches[1]; }) .filter(Boolean); const mediaProps = {}; for (let prop of templateProps) { const value = this.getAttribute(`media-${kebabCase(prop)}`); // Attrs that are not set get a prop value of false for the template. mediaProps[prop] = value == null ? false : value; } render( template({ sizes: this.sizes, height: this.offsetHeight, aspectRatio: this.aspectRatio, template: this.template, localName: this.media.localName, ...mediaProps, }), this.shadowRoot ); } get sizes() { return this.getAttribute('sizes').split(' '); } } if (!customElements.get('ms-sizes')) { customElements.define('ms-sizes', MsSizes); } export { MsSizes };