@v4fire/client
Version:
V4Fire client core library
298 lines (239 loc) • 6.46 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
/**
* [[include:base/b-image/README.md]]
* @packageDocumentation
*/
import symbolGenerator from 'core/symbol';
import { getSrcSet } from 'core/html';
import type { TaskCtx } from 'core/async';
import iProgress from 'traits/i-progress/i-progress';
import iVisible from 'traits/i-visible/i-visible';
import iBlock, { component, prop, hook, wait, ModsDecl } from 'super/i-block/i-block';
import type { SizeType } from 'base/b-image/interface';
export * from 'super/i-block/i-block';
export * from 'base/b-image/interface';
export const
$$ = symbolGenerator();
export default class bImage extends iBlock implements iProgress, iVisible {
/** @see [[iVisible.prototype.hideIfOffline]] */
readonly hideIfOffline: boolean = false;
override readonly rootTag: string = 'span';
/**
* Image src (a fallback if `srcset` provided)
*/
readonly src: string = '';
/**
* Image `srcset` attribute
*/
readonly srcset?: Dictionary<string>;
/**
* Image `sizes` attribute
*/
readonly sizes?: string;
/**
* Alternate text for the image
*/
readonly alt?: string;
/**
* Image background size type
*/
readonly sizeType: SizeType = 'contain';
/**
* Image background position
*/
readonly position: string = '50% 50%';
/**
* Image aspect ratio
*/
readonly ratio?: number;
/**
* Style (backgroundImage) before the image background
*/
readonly beforeImg?: CanArray<string>;
/**
* Style (backgroundImage) after the image background
*/
readonly afterImg?: CanArray<string>;
/**
* Parameters for an overlay image
* (when the image is loading)
*/
readonly overlayImg?: string | Dictionary;
/**
* Parameters for a broken image
* (when the image loading was failed)
*/
readonly brokenImg?: string | Dictionary;
override get rootAttrs(): Dictionary {
return {
...super['rootAttrsGetter'](),
role: 'img',
'aria-label': this.alt
};
}
static override readonly mods: ModsDecl = {
...iProgress.mods,
...iVisible.mods,
showError: [
'true',
'false'
]
};
protected override readonly $refs!: {
img: HTMLImageElement;
};
/**
* Initializes the image loading process
*/
protected init(): CanPromise<void> {
const
tmpSrc = <CanUndef<string>>this.tmp[this.src];
if (tmpSrc != null) {
this.updateHeight(tmpSrc);
this.onImageLoadSuccess(tmpSrc);
return;
}
void this.setMod('progress', true);
const img = new Image();
img.src = this.src;
if (this.srcset) {
img.srcset = getSrcSet(this.srcset);
}
if (this.sizes != null) {
img.sizes = this.sizes;
}
if (this.alt != null) {
img.alt = this.alt;
}
this.updateHeight(img);
this.async
.promise(img.init, {label: $$.loadImage})
.then(() => this.onImageLoadSuccess(img), this.onImageLoadFailed.bind(this));
}
/**
* Updates an image height according to its ratio
* @param img
*/
protected updateHeight(img: HTMLImageElement | string): void {
const
{img: imgRef} = this.$refs;
let
tmpPadding = this.tmp[`${this.src}-padding`];
if (!Object.isTruly(tmpPadding)) {
if (this.ratio != null && this.ratio !== 0) {
tmpPadding = `${(1 / this.ratio) * 100}%`;
} else if (!Object.isString(img) && this.ratio !== 0) {
tmpPadding = '100%';
} else {
tmpPadding = '';
}
}
Object.assign(imgRef.style, Object.isTruly(tmpPadding) ?
{paddingBottom: tmpPadding} :
{height: '100%'});
}
/**
* Updates an image ratio according to its height and width
* @param img
*/
protected updateCalculatedImageRatio(img: HTMLImageElement): void {
const
{img: imgRef} = this.$refs;
if (img.naturalHeight !== 0 || img.naturalWidth !== 0) {
const ratio = img.naturalHeight === 0 ? 1 : img.naturalWidth / img.naturalHeight;
imgRef.style.paddingBottom = `${(1 / ratio) * 100}%`;
}
}
/**
* Saves image styles to the cache
*/
protected memoizeImage(): CanPromise<void> {
const
{img} = this.$refs;
if (Object.isTruly(img.style.backgroundImage)) {
this.tmp[this.src] = img[$$.img];
this.tmp[`${this.src}-padding`] = img.style.paddingBottom;
}
}
protected override initModEvents(): void {
super.initModEvents();
iProgress.initModEvents(this);
iVisible.initModEvents(this);
}
/**
* Handler: image loading has successfully completed
*
* @param img
* @emits `loadSuccess()`
*/
protected onImageLoadSuccess(img: HTMLImageElement | string): void {
let
cssImg = '';
if (!Object.isString(img)) {
if (this.ratio == null) {
this.updateCalculatedImageRatio(img);
}
const
// IE has no currentSrc in HTMLImageElement so its type from lib.dom.d.ts is incorrect
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
imgUrl = img.currentSrc ?? img.src;
cssImg = `url("${imgUrl}")`;
} else {
cssImg = img;
}
const
{img: imgRef} = this.$refs;
void this.setMod('progress', false);
void this.setMod('showError', false);
imgRef[$$.img] = cssImg;
Object.assign(imgRef.style, {
backgroundImage: Array.concat([], this.beforeImg, cssImg, this.afterImg).join(','),
backgroundSize: this.sizeType,
backgroundPosition: this.position
});
this.emit('loadSuccess');
}
/**
* Handler: image loading has failed
*
* @param err
* @emits `loadFail(err: Error)`
*/
protected onImageLoadFailed(err: CanUndef<Error | TaskCtx>): void {
void this.setMod('progress', false);
if (err && 'type' in err && err.type === 'clearAsync') {
return;
}
void this.setMod('showError', true);
this.emitError('loadFail', err);
}
protected override beforeDestroy(): void {
this.$refs.img.style.backgroundImage = '';
super.beforeDestroy();
}
}