UNPKG

@v4fire/client

Version:

V4Fire client core library

267 lines (226 loc) • 6.3 kB
/*! * 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 ?? ''; } }