mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-23 22:11:45 -06:00
148 lines
5.0 KiB
TypeScript
148 lines
5.0 KiB
TypeScript
import { applyIcon } from '../designApplication';
|
|
import { ComponentBase, ComponentState } from './componentBase';
|
|
|
|
const FILL_MODE_TO_OBJECT_FIT = {
|
|
fit: 'contain',
|
|
stretch: 'fill',
|
|
zoom: 'cover',
|
|
} as const;
|
|
|
|
export type ImageState = ComponentState & {
|
|
_type_: 'Image-builtin';
|
|
fill_mode?: keyof typeof FILL_MODE_TO_OBJECT_FIT;
|
|
imageUrl?: string;
|
|
reportError?: boolean;
|
|
corner_radius?: [number, number, number, number];
|
|
accessibility_description?: string;
|
|
};
|
|
|
|
export class ImageComponent extends ComponentBase {
|
|
state: Required<ImageState>;
|
|
|
|
private imageElement: HTMLImageElement;
|
|
private resizeObserver: ResizeObserver;
|
|
|
|
createElement(): HTMLElement {
|
|
let element = document.createElement('div');
|
|
element.classList.add('rio-image');
|
|
|
|
this.imageElement = document.createElement('img');
|
|
this.imageElement.role = 'img';
|
|
// Dragging prevents mouseups and is annoying in general, so we'll
|
|
// disable it
|
|
this.imageElement.draggable = false;
|
|
element.appendChild(this.imageElement);
|
|
|
|
this.imageElement.onload = this._onLoad.bind(this);
|
|
this.imageElement.onerror = this._onError.bind(this);
|
|
|
|
this.resizeObserver = new ResizeObserver(this._updateSize.bind(this));
|
|
this.resizeObserver.observe(element);
|
|
|
|
return element;
|
|
}
|
|
|
|
onDestruction(): void {
|
|
this.resizeObserver.disconnect();
|
|
}
|
|
|
|
updateElement(
|
|
deltaState: ImageState,
|
|
latentComponents: Set<ComponentBase>
|
|
): void {
|
|
super.updateElement(deltaState, latentComponents);
|
|
|
|
let imgElement = this.imageElement;
|
|
|
|
if (
|
|
deltaState.imageUrl !== undefined &&
|
|
imgElement.src !== deltaState.imageUrl
|
|
) {
|
|
// this.element.classList.add('rio-content-loading');
|
|
imgElement.src = deltaState.imageUrl;
|
|
|
|
// Until the image is loaded and we get access to its resolution,
|
|
// let it fill the entire space. This is the correct size for all
|
|
// `fill_mode`s except `"fit"` anyway, so there's no harm in setting
|
|
// it now rather than later. (SVGs might temporarily render content
|
|
// outside of the viewbox, but the only way to prevent that would be
|
|
// to make the image invisible until loaded.)
|
|
this.imageElement.style.width = '100%';
|
|
this.imageElement.style.height = '100%';
|
|
|
|
// If we're currently displaying an error icon, remove it
|
|
if (this.element.firstElementChild !== imgElement) {
|
|
this.element.firstElementChild!.remove();
|
|
this.element.appendChild(imgElement);
|
|
}
|
|
}
|
|
|
|
if (deltaState.fill_mode !== undefined) {
|
|
imgElement.style.objectFit =
|
|
FILL_MODE_TO_OBJECT_FIT[deltaState.fill_mode];
|
|
|
|
this._updateSize();
|
|
}
|
|
|
|
if (deltaState.corner_radius !== undefined) {
|
|
let [topLeft, topRight, bottomRight, bottomLeft] =
|
|
deltaState.corner_radius;
|
|
|
|
imgElement.style.borderRadius = `${topLeft}rem ${topRight}rem ${bottomRight}rem ${bottomLeft}rem`;
|
|
}
|
|
|
|
if (deltaState.accessibility_description !== undefined) {
|
|
imgElement.alt = deltaState.accessibility_description;
|
|
}
|
|
}
|
|
|
|
private _onLoad(): void {
|
|
// this.element.classList.remove('rio-content-loading');
|
|
this._updateSize();
|
|
}
|
|
|
|
private _updateSize(): void {
|
|
// We need to resize the `<img>` element to the size of the image,
|
|
// because:
|
|
// 1. It ensures that `corner_radius` is always visible, even if too
|
|
// much space has been allocated
|
|
// 2. Browsers are dumb and render content outside of the SVG viewbox if
|
|
// the <img> element is too large
|
|
if (this.state.fill_mode === 'fit') {
|
|
let rect = this.element.getBoundingClientRect();
|
|
let aspectRatioAvailable = rect.width / rect.height;
|
|
let aspectRatioImage =
|
|
this.imageElement.naturalWidth /
|
|
this.imageElement.naturalHeight;
|
|
|
|
let scaleFactor =
|
|
aspectRatioAvailable > aspectRatioImage
|
|
? rect.height / this.imageElement.naturalHeight
|
|
: rect.width / this.imageElement.naturalWidth;
|
|
|
|
let imgWidth = Math.round(
|
|
this.imageElement.naturalWidth * scaleFactor
|
|
);
|
|
let imgHeight = Math.round(
|
|
this.imageElement.naturalHeight * scaleFactor
|
|
);
|
|
|
|
this.imageElement.style.width = `${imgWidth}px`;
|
|
this.imageElement.style.height = `${imgHeight}px`;
|
|
} else {
|
|
this.imageElement.style.width = '100%';
|
|
this.imageElement.style.height = '100%';
|
|
}
|
|
}
|
|
|
|
private _onError(event: string | Event): void {
|
|
this.element.classList.remove('rio-content-loading');
|
|
|
|
applyIcon(this.element, 'material/broken_image');
|
|
|
|
this.sendMessageToBackend({
|
|
type: 'onError',
|
|
});
|
|
}
|
|
}
|