Skip to content

Commit

Permalink
feat(lr-img): preview blur
Browse files Browse the repository at this point in the history
* feat: Blurred image preview for <lr-img> during image loading

* feat: Add integrate analytics functionality to capture insights for (LR) image

* fix: Revert init lazy params and replace async/await

* fix: Added analytics for CDN operaions and handler errors img

* fix: corrected option params in method _getUrlBase
  • Loading branch information
Egor Didenko authored Jan 26, 2024
1 parent 47274fe commit f42967a
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 41 deletions.
2 changes: 1 addition & 1 deletion blocks/Img/Img.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class Img extends ImgBase {
});

this.sub$$('lazy', (val) => {
if (!this.$$('is-background-for')) {
if (!this.$$('is-background-for') && !this.$$('is-preview-blur')) {
this.img.loading = val ? 'lazy' : 'eager';
}
});
Expand Down
198 changes: 158 additions & 40 deletions blocks/Img/ImgBase.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { applyTemplateData } from '../../utils/template-utils.js';
import { createCdnUrl, createCdnUrlModifiers, createOriginalUrl } from '../../utils/cdn-utils.js';
import { PROPS_MAP } from './props-map.js';
import { stringToArray } from '../../utils/stringToArray.js';
import { uniqueArray } from '../../utils/uniqueArray.js';
import { parseObjectToString } from './utils/parseObjectToString.js';
import { ImgConfig } from './ImgConfig.js';
import { DEV_MODE, HI_RES_K, ULTRA_RES_K, UNRESOLVED_ATTR, MAX_WIDTH, MAX_WIDTH_JPG } from './configurations.js';
import {
DEV_MODE,
HI_RES_K,
ULTRA_RES_K,
UNRESOLVED_ATTR,
MAX_WIDTH,
MAX_WIDTH_JPG,
ImgTypeEnum,
} from './configurations.js';

export class ImgBase extends ImgConfig {
_img = new Image();
_imgPreview = new Image();

/**
* @private
* @param {String} src
Expand All @@ -27,7 +37,7 @@ export class ImgBase extends ImgConfig {
* @returns {String | Number}
*/
_validateSize(size) {
if (size.trim() !== '') {
if (size?.trim() !== '') {
// Extract numeric part
let numericPart = size.match(/\d+/)[0];

Expand Down Expand Up @@ -59,17 +69,19 @@ export class ImgBase extends ImgConfig {
resize: this._validateSize(size),
blur,
'cdn-operations': this.$$('cdn-operations'),
analytics: this.analyticsParams(),
};

return createCdnUrlModifiers(...parseObjectToString(params));
}

/**
* @private
* @param {String} size
* @param {String} [size]
* @param {String} [blur]
* @returns {any}
*/
_getUrlBase(size = '') {
_getUrlBase(size = '', blur = '') {
if (this.$$('src').startsWith('data:') || this.$$('src').startsWith('blob:')) {
return this.$$('src');
}
Expand All @@ -79,7 +91,7 @@ export class ImgBase extends ImgConfig {
return this._proxyUrl(this.$$('src'));
}

let cdnModifiers = this._getCdnModifiers(size);
let cdnModifiers = this._getCdnModifiers(size, blur);

if (this.$$('src').startsWith(this.$$('cdn-cname'))) {
return createCdnUrl(this.$$('src'), cdnModifiers);
Expand Down Expand Up @@ -158,6 +170,7 @@ export class ImgBase extends ImgConfig {
let rect = el.getBoundingClientRect();
let w = k * Math.round(rect.width);
let h = wOnly ? '' : k * Math.round(rect.height);

if (w || h) {
return `${w ? w : ''}x${h ? h : ''}`;
} else {
Expand All @@ -181,30 +194,31 @@ export class ImgBase extends ImgConfig {

/** @type {HTMLImageElement} */
get img() {
if (!this._img) {
/** @private */
this._img = new Image();
this._setupEventProxy(this.img);
this._img.setAttribute(UNRESOLVED_ATTR, '');
this.img.onload = () => {
this.img.removeAttribute(UNRESOLVED_ATTR);
};
this.initAttributes();
if (!this.hasPreviewImage) {
this._setupConfigForImage({ elNode: this._img });
this.appendChild(this._img);
}
return this._img;
}

get bgSelector() {
return this.$$('is-background-for');
get currentImg() {
return this.hasPreviewImage
? {
type: ImgTypeEnum.PREVIEW,
img: this._imgPreview,
}
: {
type: ImgTypeEnum.MAIN,
img: this.img,
};
}

initAttributes() {
[...this.attributes].forEach((attr) => {
if (!PROPS_MAP[attr.name]) {
this.img.setAttribute(attr.name, attr.value);
}
});
get hasPreviewImage() {
return this.$$('is-preview-blur');
}

get bgSelector() {
return this.$$('is-background-for');
}

get breakpoints() {
Expand Down Expand Up @@ -251,12 +265,12 @@ export class ImgBase extends ImgConfig {
}
});
} else {
srcset.add(this._getUrlBase(this._getElSize(this.img)) + ' 1x');
srcset.add(this._getUrlBase(this._getElSize(this.currentImg.img)) + ' 1x');
if (this.$$('hi-res-support')) {
srcset.add(this._getUrlBase(this._getElSize(this.img, 2)) + ' 2x');
srcset.add(this._getUrlBase(this._getElSize(this.currentImg.img, 2)) + ' 2x');
}
if (this.$$('ultra-res-support')) {
srcset.add(this._getUrlBase(this._getElSize(this.img, 3)) + ' 3x');
srcset.add(this._getUrlBase(this._getElSize(this.currentImg.img, 3)) + ' 3x');
}
}
return [...srcset].join();
Expand All @@ -266,25 +280,129 @@ export class ImgBase extends ImgConfig {
return this._getUrlBase();
}

init() {
if (this.bgSelector) {
[...document.querySelectorAll(this.bgSelector)].forEach((el) => {
if (this.$$('intersection')) {
this.initIntersection(el, () => {
this.renderBg(el);
});
} else {
get srcUrlPreview() {
return this._getUrlBase('100x', '100');
}

renderBackground() {
[...document.querySelectorAll(this.bgSelector)].forEach((el) => {
if (this.$$('intersection')) {
this.initIntersection(el, () => {
this.renderBg(el);
});
} else {
this.renderBg(el);
}
});
}

_appendURL({ elNode, src, srcset }) {
if (src) {
elNode.src = src;
}

if (srcset) {
elNode.srcset = srcset;
}
}

_setupConfigForImage({ elNode }) {
this._setupEventProxy(elNode);
this.initAttributes(elNode);
}

loaderImage({ src, srcset, elNode }) {
return new Promise((resolve, reject) => {
this._setupConfigForImage({ elNode });

elNode.setAttribute(UNRESOLVED_ATTR, '');

elNode.addEventListener('load', () => {
elNode.removeAttribute(UNRESOLVED_ATTR);
resolve(elNode);
});

elNode.addEventListener('error', () => {
reject(false);
});

this._appendURL({
elNode,
src,
srcset,
});
});
}

async renderImage() {
if (this.$$('intersection')) {
if (this.hasPreviewImage) {
this._setupConfigForImage({ elNode: this._imgPreview });
this.appendChild(this._imgPreview);
}

this.initIntersection(this.currentImg.img, async () => {
if (this.hasPreviewImage) {
this._imgPreview.src = this.srcUrlPreview;
}

try {
await this.loaderImage({
src: this.getSrc(),
srcset: this.getSrcset(),
elNode: this._img,
});

if (this.hasPreviewImage) {
await this._imgPreview.remove();
}

this.appendChild(this._img);
} catch (e) {
if (this.hasPreviewImage) {
await this._imgPreview?.remove();
}
this.appendChild(this._img);
}
});
} else if (this.$$('intersection')) {
this.initIntersection(this.img, () => {
this.img.srcset = this.getSrcset();
this.img.src = this.getSrc();

return;
}

try {
if (this.hasPreviewImage) {
await this.loaderImage({
src: this.srcUrlPreview,
elNode: this._imgPreview,
});

this.appendChild(this._imgPreview);
}

await this.loaderImage({
src: this.getSrc(),
srcset: this.getSrcset(),
elNode: this._img,
});

if (this.hasPreviewImage) {
await this._imgPreview?.remove();
}

this.appendChild(this._img);
} catch (e) {
if (this.hasPreviewImage) {
await this._imgPreview?.remove();
}
this.appendChild(this._img);
}
}

init() {
if (this.bgSelector) {
this.renderBackground();
} else {
this.img.srcset = this.getSrcset();
this.img.src = this.getSrc();
this.renderImage();
}
}
}
13 changes: 13 additions & 0 deletions blocks/Img/ImgConfig.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseComponent, Data } from '@symbiotejs/symbiote';
import { PROPS_MAP } from './props-map.js';
import { CSS_PREF } from './configurations.js';
import { PACKAGE_NAME, PACKAGE_VERSION } from '../../env.js';

const CSS_PROPS = Object.create(null);
for (let prop in PROPS_MAP) {
Expand Down Expand Up @@ -40,6 +41,18 @@ export class ImgConfig extends BaseComponent {
});
}

analyticsParams() {
return `-/@clib/${PACKAGE_NAME}/${PACKAGE_VERSION}/lr-img/`;
}

initAttributes(el) {
[...this.attributes].forEach((attr) => {
if (!PROPS_MAP[attr.name]) {
el.setAttribute(attr.name, attr.value);
}
});
}

/**
* @param {HTMLElement} el
* @param {() => void} cbkFn
Expand Down
5 changes: 5 additions & 0 deletions blocks/Img/configurations.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ export const DEV_MODE =

export const MAX_WIDTH = 3000;
export const MAX_WIDTH_JPG = 5000;

export const ImgTypeEnum = Object.freeze({
PREVIEW: 'PREVIEW',
MAIN: 'MAIN',
});
3 changes: 3 additions & 0 deletions blocks/Img/props-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ export const PROPS_MAP = Object.freeze({
progressive: {},
quality: {},
'is-background-for': {},
'is-preview-blur': {
default: 1,
},
});
3 changes: 3 additions & 0 deletions blocks/Img/utils/parseObjectToString.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const parseObjectToString = (params) =>
if (key === 'cdn-operations') {
return value;
}
if (key === 'analytics') {
return value;
}

return `${key}/${value}`;
});
4 changes: 4 additions & 0 deletions utils/cdn-utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PACKAGE_NAME, PACKAGE_VERSION } from '../env.js';

/**
* Trim leading `-/`, `/` and trailing `/` from CDN operation
*
Expand Down Expand Up @@ -153,6 +155,8 @@ export function splitFileUrl(fileUrl) {
* @param {String} [filename] - Filename for CDN or file URL for Proxy, will override one from `baseCdnUrl`
* @returns {String}
*/

// TODO eadidenko replace arg to pass the object parameter
export const createCdnUrl = (baseCdnUrl, cdnModifiers, filename) => {
let url = new URL(trimFilename(baseCdnUrl));
filename = filename || extractFilename(baseCdnUrl);
Expand Down

0 comments on commit f42967a

Please sign in to comment.