UNPKG

vira

Version:

A simple and highly versatile design system using element-vir.

145 lines (139 loc) 4.67 kB
import { wait } from '@augment-vir/common'; import { classMap, css, defineElementEvent, html, listen, renderIf } from 'element-vir'; import { LoaderAnimated24Icon, StatusFailure24Icon } from '../icons/index.js'; import { defineViraElement } from './define-vira-element.js'; import { ViraIcon } from './vira-icon.element.js'; /** * An `<img>` element wrapper that handles size constraints and includes slots for loading and error * indicators. * * Use CSS properties to constrain the image. In particular, set `min-height` and `min-width` on * this to control the size of the loader and error slots. * * @category Image * @category Elements * @see https://electrovir.github.io/element-vir/vira/book/elements/vira-image */ export const ViraImage = defineViraElement()({ tagName: 'vira-image', state() { return { /** * To avoid race conditions between `<img>` element events and potential input changing, * save the loaded state of an URL's image by the image's URL. */ loadedUrls: {}, /** * To avoid race conditions between `<img>` element events and potential input changing, * save the errored state of an URL's image by the image's URL. */ erroredUrls: {}, }; }, hostClasses: { 'vira-image-height-constrained': ({ inputs }) => inputs.dominantDimension === 'height', }, slotNames: [ 'loading', 'error', ], events: { imageLoad: defineElementEvent(), imageError: defineElementEvent(), }, styles: ({ hostClasses }) => css ` :host { display: inline-flex; overflow: hidden; flex-direction: column; justify-content: center; position: relative; border-radius: inherit; min-height: 100px; min-width: 100px; } img { width: 100%; height: auto; flex-shrink: 0; } ${hostClasses['vira-image-height-constrained'].selector} { flex-direction: row; } ${hostClasses['vira-image-height-constrained'].selector} img { width: auto; height: 100%; } .status-wrapper { overflow: hidden; border-radius: inherit; width: 100%; height: 100%; position: absolute; top: 0; left: 0; display: flex; justify-content: center; align-items: center; } .error { color: red; } .hidden { display: none; } `, render({ inputs, state, updateState, dispatch, events, slotNames }) { /** * Saved off for use in the image listeners. This is used to eliminate race conditions * between image events and the input URL changing. */ const imageUrl = inputs.imageUrl; const statusTemplate = state.erroredUrls[imageUrl] ? html ` <slot class="status-wrapper" name=${slotNames.error}> <${ViraIcon.assign({ icon: StatusFailure24Icon })} class="error"></${ViraIcon}> </slot> ` : state.loadedUrls[imageUrl] ? undefined : html ` <slot class="status-wrapper" name=${slotNames.loading}> <${ViraIcon.assign({ icon: LoaderAnimated24Icon })}></${ViraIcon}> </slot> `; return html ` ${renderIf(!!statusTemplate, statusTemplate)} <img class=${classMap({ hidden: !!statusTemplate, })} ${listen('load', async () => { if (inputs._debugLoadDelay) { await wait(inputs._debugLoadDelay); } updateState({ loadedUrls: { ...state.loadedUrls, [imageUrl]: true, }, }); dispatch(new events.imageLoad()); })} ${listen('error', async (event) => { if (inputs._debugLoadDelay) { await wait(inputs._debugLoadDelay); } updateState({ erroredUrls: { ...state.erroredUrls, [imageUrl]: true, }, }); dispatch(new events.imageError(event.error)); })} src=${imageUrl} /> `; }, });