diff --git a/blocks/Img/ImgBase.js b/blocks/Img/ImgBase.js index 9343517da..030267a67 100644 --- a/blocks/Img/ImgBase.js +++ b/blocks/Img/ImgBase.js @@ -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} 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 @@ -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)); } /** @@ -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); @@ -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 { @@ -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; - }); - } } diff --git a/blocks/Img/ImgConfig.js b/blocks/Img/ImgConfig.js new file mode 100644 index 000000000..cc9c1060e --- /dev/null +++ b/blocks/Img/ImgConfig.js @@ -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} 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; + }); + } +} diff --git a/blocks/Img/configurations.js b/blocks/Img/configurations.js new file mode 100644 index 000000000..a379bb98b --- /dev/null +++ b/blocks/Img/configurations.js @@ -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; diff --git a/blocks/Img/props-map.js b/blocks/Img/props-map.js index 3c37a92e4..535b5e79a 100644 --- a/blocks/Img/props-map.js +++ b/blocks/Img/props-map.js @@ -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': {}, }); diff --git a/blocks/Img/utils/parseObjectToString.js b/blocks/Img/utils/parseObjectToString.js new file mode 100644 index 000000000..5e3de4335 --- /dev/null +++ b/blocks/Img/utils/parseObjectToString.js @@ -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}`; + });