Skip to content

Commit

Permalink
refactor: Refactoring lr-img
Browse files Browse the repository at this point in the history
Verify image size compatibility for the CDN.
Exclude "auto" and "smart" values from the format/quality settings.
Eliminate breakpoints for the background image.
Establish ImgConfig.
  • Loading branch information
egordidenko committed Dec 27, 2023
1 parent bac70e5 commit ea3fec9
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 129 deletions.
182 changes: 59 additions & 123 deletions blocks/Img/ImgBase.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,13 @@
import { BaseComponent, Data } from '@symbiotejs/symbiote';
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';

const CSS_PREF = '--lr-img-';
const UNRESOLVED_ATTR = 'unresolved';
const HI_RES_K = 2;
const ULTRA_RES_K = 3;
const DEV_MODE =
!window.location.host.trim() || window.location.host.includes(':') || window.location.hostname.includes('localhost');

const CSS_PROPS = Object.create(null);
for (let prop in PROPS_MAP) {
CSS_PROPS[CSS_PREF + prop] = PROPS_MAP[prop]?.default || '';
}

export class ImgBase extends BaseComponent {
cssInit$ = CSS_PROPS;

/**
* @param {String} key
* @returns {any}
*/
$$(key) {
return this.$[CSS_PREF + key];
}

/** @param {Object<String, String | Number>} kvObj */
set$$(kvObj) {
for (let key in kvObj) {
this.$[CSS_PREF + key] = kvObj[key];
}
}

/**
* @param {String} key
* @param {(val: any) => void} kbFn
*/
sub$$(key, kbFn) {
this.sub(CSS_PREF + key, (val) => {
// null comes from CSS context property
// empty string comes from attribute value
if (val === null || val === '') {
return;
}
kbFn(val);
});
}

export class ImgBase extends ImgConfig {
/**
* @private
* @param {String} src
Expand All @@ -62,19 +20,48 @@ export class ImgBase extends BaseComponent {
return src;
}

/**
* Validate size
*
* @param {String} [size]
* @returns {String | Number}
*/
_validateSize(size) {
if (size.trim() !== '') {
// Extract numeric part
let numericPart = size.match(/\d+/)[0];

// Extract alphabetic part
let alphabeticPart = size.match(/[a-zA-Z]+/)[0];

const bp = parseInt(numericPart, 10);

if (Number(bp) > MAX_WIDTH_JPG && this.hasFormatJPG) {
return MAX_WIDTH_JPG + alphabeticPart;
} else if (Number(bp) > MAX_WIDTH && !this.hasFormatJPG) {
return MAX_WIDTH + alphabeticPart;
}
}

return size;
}

/**
* Image operations
*
* @param {String} [size]
* @param {String} [blur]
*/
_getCdnModifiers(size = '') {
return createCdnUrlModifiers(
//
size && `resize/${size}`,
this.$$('cdn-operations') || '',
`format/${this.$$('format') || PROPS_MAP.format.default}`,
`quality/${this.$$('quality') || PROPS_MAP.quality.default}`
);
_getCdnModifiers(size, blur) {
const params = {
format: this.$$('format'),
quality: this.$$('quality'),
resize: this._validateSize(size),
blur,
'cdn-operations': this.$$('cdn-operations'),
};

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

/**
Expand Down Expand Up @@ -222,34 +209,30 @@ export class ImgBase extends BaseComponent {

get breakpoints() {
if (this.$$('breakpoints')) {
return uniqueArray(stringToArray(this.$$('breakpoints')).map((str) => Number(str)));
const list = stringToArray(this.$$('breakpoints'));
return uniqueArray(list.map((bp) => parseInt(bp, 10)));
} else {
return null;
}
}

get hasFormatJPG() {
return this.$$('format').toLowerCase() === 'jpeg';
}

/** @param {HTMLElement} el */
renderBg(el) {
let imgSet = new Set();
if (this.breakpoints) {
this.breakpoints.forEach((bp) => {
imgSet.add(`url("${this._getUrlBase(bp + 'x')}") ${bp}w`);
if (this.$$('hi-res-support')) {
imgSet.add(`url("${this._getUrlBase(bp * HI_RES_K + 'x')}") ${bp * HI_RES_K}w`);
}
if (this.$$('ultra-res-support')) {
imgSet.add(`url("${this._getUrlBase(bp * ULTRA_RES_K + 'x')}") ${bp * ULTRA_RES_K}w`);
}
});
} else {
imgSet.add(`url("${this._getUrlBase(this._getElSize(el))}") 1x`);
if (this.$$('hi-res-support')) {
imgSet.add(`url("${this._getUrlBase(this._getElSize(el, HI_RES_K))}") ${HI_RES_K}x`);
}
if (this.$$('ultra-res-support')) {
imgSet.add(`url("${this._getUrlBase(this._getElSize(el, ULTRA_RES_K))}") ${ULTRA_RES_K}x`);
}

imgSet.add(`url("${this._getUrlBase(this._getElSize(el))}") 1x`);
if (this.$$('hi-res-support')) {
imgSet.add(`url("${this._getUrlBase(this._getElSize(el, HI_RES_K))}") ${HI_RES_K}x`);
}

if (this.$$('ultra-res-support')) {
imgSet.add(`url("${this._getUrlBase(this._getElSize(el, ULTRA_RES_K))}") ${ULTRA_RES_K}x`);
}

let iSet = `image-set(${[...imgSet].join(', ')})`;
el.style.setProperty('background-image', iSet);
el.style.setProperty('background-image', '-webkit-' + iSet);
Expand All @@ -259,12 +242,12 @@ export class ImgBase extends BaseComponent {
let srcset = new Set();
if (this.breakpoints) {
this.breakpoints.forEach((bp) => {
srcset.add(this._getUrlBase(bp + 'x') + ` ${bp}w`);
srcset.add(this._getUrlBase(bp + 'x') + ` ${this._validateSize(bp + 'w')}`);
if (this.$$('hi-res-support')) {
srcset.add(this._getUrlBase(bp * HI_RES_K + 'x') + ` ${bp * HI_RES_K}w`);
srcset.add(this._getUrlBase(bp * HI_RES_K + 'x') + ` ${this._validateSize(bp * HI_RES_K + 'w')}`);
}
if (this.$$('ultra-res-support')) {
srcset.add(this._getUrlBase(bp * ULTRA_RES_K + 'x') + ` ${bp * ULTRA_RES_K}w`);
srcset.add(this._getUrlBase(bp * ULTRA_RES_K + 'x') + ` ${this._validateSize(bp * ULTRA_RES_K + 'w')}`);
}
});
} else {
Expand Down Expand Up @@ -304,51 +287,4 @@ export class ImgBase extends BaseComponent {
this.img.src = this.getSrc();
}
}

/**
* @param {HTMLElement} el
* @param {() => void} cbkFn
*/
initIntersection(el, cbkFn) {
let opts = {
root: null,
rootMargin: '0px',
};
/** @private */
this._isnObserver = new IntersectionObserver((entries) => {
entries.forEach((ent) => {
if (ent.isIntersecting) {
cbkFn();
this._isnObserver.unobserve(el);
}
});
}, opts);
this._isnObserver.observe(el);
if (!this._observed) {
/** @private */
this._observed = new Set();
}
this._observed.add(el);
}

destroyCallback() {
super.destroyCallback();
if (this._isnObserver) {
this._observed.forEach((el) => {
this._isnObserver.unobserve(el);
});
this._isnObserver = null;
}
Data.deleteCtx(this);
}

static get observedAttributes() {
return Object.keys(PROPS_MAP);
}

attributeChangedCallback(name, oldVal, newVal) {
window.setTimeout(() => {
this.$[CSS_PREF + name] = newVal;
});
}
}
89 changes: 89 additions & 0 deletions blocks/Img/ImgConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BaseComponent, Data } from '@symbiotejs/symbiote';
import { PROPS_MAP } from './props-map.js';
import { CSS_PREF } from './configurations.js';

const CSS_PROPS = Object.create(null);
for (let prop in PROPS_MAP) {
CSS_PROPS[CSS_PREF + prop] = PROPS_MAP[prop]?.default || '';
}

export class ImgConfig extends BaseComponent {
cssInit$ = CSS_PROPS;

/**
* @param {String} key
* @returns {any}
*/
$$(key) {
return this.$[CSS_PREF + key];
}

/** @param {Object<String, String | Number>} kvObj */
set$$(kvObj) {
for (let key in kvObj) {
this.$[CSS_PREF + key] = kvObj[key];
}
}

/**
* @param {String} key
* @param {(val: any) => void} kbFn
*/
sub$$(key, kbFn) {
this.sub(CSS_PREF + key, (val) => {
// null comes from CSS context property
// empty string comes from attribute value
if (val === null || val === '') {
return;
}
kbFn(val);
});
}

/**
* @param {HTMLElement} el
* @param {() => void} cbkFn
*/
initIntersection(el, cbkFn) {
let opts = {
root: null,
rootMargin: '0px',
};
/** @private */
this._isnObserver = new IntersectionObserver((entries) => {
entries.forEach((ent) => {
if (ent.isIntersecting) {
cbkFn();
this._isnObserver.unobserve(el);
}
});
}, opts);
this._isnObserver.observe(el);
if (!this._observed) {
/** @private */
this._observed = new Set();
}
this._observed.add(el);
}

destroyCallback() {
super.destroyCallback();
if (this._isnObserver) {
this._observed.forEach((el) => {
this._isnObserver.unobserve(el);
});
this._isnObserver = null;
}
Data.deleteCtx(this);
}

static get observedAttributes() {
return Object.keys(PROPS_MAP);
}

attributeChangedCallback(name, oldVal, newVal) {
window.setTimeout(() => {
this.$[CSS_PREF + name] = newVal;
});
}
}
9 changes: 9 additions & 0 deletions blocks/Img/configurations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const CSS_PREF = '--lr-img-';
export const UNRESOLVED_ATTR = 'unresolved';
export const HI_RES_K = 2;
export const ULTRA_RES_K = 3;
export const DEV_MODE =
!window.location.host.trim() || window.location.host.includes(':') || window.location.hostname.includes('localhost');

export const MAX_WIDTH = 3000;
export const MAX_WIDTH_JPG = 5000;
8 changes: 2 additions & 6 deletions blocks/Img/props-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,9 @@ export const PROPS_MAP = Object.freeze({
default: 1,
},
'ultra-res-support': {}, // ?
format: {
default: 'auto',
},
format: {},
'cdn-operations': {},
progressive: {},
quality: {
default: 'smart',
},
quality: {},
'is-background-for': {},
});
10 changes: 10 additions & 0 deletions blocks/Img/utils/parseObjectToString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const parseObjectToString = (params) =>
Object.entries(params)
.filter(([key, value]) => value !== undefined && value !== '')
.map(([key, value]) => {
if (key === 'cdn-operations') {
return value;
}

return `${key}/${value}`;
});

0 comments on commit ea3fec9

Please sign in to comment.