@v4fire/client
Version:
V4Fire client core library
267 lines (226 loc) • 6.3 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
import { concatURLs } from 'core/url';
import { getSrcSet } from 'core/html';
import {
INIT_LOAD,
IS_LOADED,
IS_LOADING
} from 'core/dom/image/const';
import type { ImageOptions, ImageStage, ShadowElState, PictureFactoryResult } from 'core/dom/image/interface';
/**
* Helper class that provides API to create DOM elements
*/
export default class Factory {
/**
* Creates a "shadow" state to the specified element.
* The state contains the loading state, generated shadow DOM, provided options, and so on.
*
* @param el
* @param selfOptions
* @param mainOptions
* @param type
*/
shadowState(el: HTMLElement, selfOptions: ImageOptions, mainOptions: ImageOptions, type: ImageStage): ShadowElState {
let res: ShadowElState;
if (Object.isArray(selfOptions.sources) && selfOptions.sources.length > 0) {
const {picture, img} = this.picture(selfOptions, mainOptions, type);
res = {
pictureNode: picture,
imgNode: img,
isFailed: false,
selfOptions,
mainOptions,
stageType: type
};
} else {
const img = this.img(selfOptions, mainOptions, type);
res = {
pictureNode: undefined,
imgNode: img,
isFailed: false,
selfOptions,
mainOptions,
stageType: type
};
}
return res;
}
/**
* Creates a picture element with sources and an image tag
*
* @see https://developer.mozilla.org/ru/docs/Web/HTML/Element/picture
*
* @example
* ```typescript
* this.sources({
* src: 'preview.jpg',
* sources: [{srcset: 'srcset-with-webp-img', type: 'webp'}]
* }, {
* src: 'main.jpg',
* sources: [{srcset: 'srcset-with-webp-img', type: 'webp'}],
* baseSrc: 'https://path'
* });
* ```
*
* ```html
* <picture>
* <source srcset="https://path/srcset-with-webp-img" type="image/webp">
* <img src="https://path/preview.jpg">
* </picture>
* ```
*
* @param selfOptions
* @param mainOptions
* @param type
*/
picture(selfOptions: ImageOptions, mainOptions: ImageOptions, type: ImageStage): PictureFactoryResult {
const
picture = document.createElement('picture'),
img = this.img(selfOptions, mainOptions, type);
if (selfOptions.sources != null && selfOptions.sources.length > 0) {
const sourcesFragment = this.sources(selfOptions, mainOptions);
picture.appendChild(sourcesFragment);
}
picture.appendChild(img);
return {picture, img};
}
/**
* Creates source elements using the specified options to generate attributes
*
* @see https://developer.mozilla.org/ru/docs/Web/HTML/Element/source
*
* @example
* ```typescript
* // Provided options
* this.sources({
* src: 'broken.img',
* sources: [{srcset: 'srcset-with-webp-img', type: 'webp'}]
* }, {
* src: 'main.img',
* baseSrc: 'https://path'
* });
*
* // The result is a document fragment with <source srcset="https://path/srcset-with-webp-img" type="image/webp">
* ```
*
* @param selfOptions
* @param mainOptions
*/
sources(selfOptions: ImageOptions, mainOptions: ImageOptions): DocumentFragment {
const fragment = document.createDocumentFragment();
if (selfOptions.sources == null || selfOptions.sources.length === 0) {
return fragment;
}
for (let i = 0; i < selfOptions.sources.length; i++) {
const
source = selfOptions.sources[i],
sourceNode = document.createElement('source');
sourceNode.media = source.media ?? '';
sourceNode.sizes = source.sizes ?? '';
sourceNode.srcset = this.srcset(source.srcset, selfOptions, mainOptions);
sourceNode.type = this.type(source.type);
fragment.appendChild(sourceNode);
}
return fragment;
}
/**
* Creates an image element
*
* @param selfOptions
* @param mainOptions
* @param type
*/
img(selfOptions: ImageOptions, mainOptions: ImageOptions, type: ImageStage): HTMLImageElement {
const
imgNode = document.createElement('img');
/*
* Create a function to prevent immediate loading of the `broken` image
*/
imgNode[INIT_LOAD] = () => {
imgNode.sizes = selfOptions.sizes ?? '';
imgNode.src = this.src(selfOptions.src, selfOptions, mainOptions);
imgNode.srcset = this.srcset(selfOptions.srcset, selfOptions, mainOptions);
imgNode[IS_LOADING] = true;
imgNode.init.then(
() => imgNode[IS_LOADED] = true,
() => imgNode[IS_LOADED] = false
);
};
/*
* Immediate load every image except the `broken` image
*/
if (type !== 'broken') {
imgNode[INIT_LOAD]();
}
return imgNode;
}
/**
* Creates a `type` attribute value of the `source` tag
* @param type
*/
type(type: CanUndef<string>): string {
if (type == null || type === '') {
return '';
}
return `image/${type}`;
}
/**
* Creates a value of the `src` attribute
*
* @param src
* @param selfOptions
* @param mainOptions
*/
src(src: CanUndef<string>, selfOptions: ImageOptions, mainOptions: ImageOptions): string {
if (src == null || src === '') {
return '';
}
const
baseSrc = this.getBaseSrc(selfOptions, mainOptions);
if (baseSrc == null || baseSrc === '') {
return src;
}
return concatURLs(baseSrc, src);
}
/**
* Creates a value of the `srcset` attribute
*
* @param srcset
* @param selfOptions
* @param mainOptions
*/
srcset(srcset: CanUndef<Dictionary<string> | string>, selfOptions: ImageOptions, mainOptions: ImageOptions): string {
const
normalized = Object.isPlainObject(srcset) ? getSrcSet(srcset) : srcset;
if (normalized == null || normalized === '') {
return '';
}
const
baseSrc = this.getBaseSrc(selfOptions, mainOptions);
if (baseSrc == null || baseSrc === '') {
return normalized;
}
const
chunks = normalized.split(','),
newSrcset = <string[]>[];
for (let i = 0; i < chunks.length; i++) {
newSrcset.push(concatURLs(baseSrc, chunks[i].trim()));
}
return newSrcset.join(',');
}
/**
* Returns `baseSrc` from the specified options
*
* @param selfOptions
* @param mainOptions
*/
protected getBaseSrc(selfOptions: ImageOptions, mainOptions: ImageOptions): CanUndef<string> {
return selfOptions.baseSrc ?? mainOptions.baseSrc ?? '';
}
}