Last active
March 22, 2023 11:33
-
-
Save 13twelve/bc2823ea3ffcef305aa98b9f8db8fcbc to your computer and use it in GitHub Desktop.
Create responsive image sizes attribute with JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const responsiveImageSizes = (sizes, feConfig = {}, relativeUnits = true) => { | |
if (!feConfig.structure || !feConfig.structure.columns || !feConfig.structure.container || !feConfig.structure.gutters || !feConfig.structure.gutters.inner) { | |
return '100vw'; | |
} | |
// remSize - base for rem calcs | |
const remSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; | |
const remCalc = (px) => `${ parseFloat(px) / remSize }rem`; | |
// | |
const getUnitValue = (val) => { | |
var result = {}; | |
if (typeof val === 'number') { | |
result.value = val; | |
} | |
if (typeof val === 'string') { | |
result.value = parseFloat(val); | |
result.unit = val.substr(('' + result.value).length).trim(); | |
result.unit = result.unit ? result.unit : null; | |
} | |
return result; | |
}; | |
// parse CSS type data from config | |
const cssColumns = feConfig.structure.columns; | |
const cssContainerWidths = feConfig.structure.container; | |
const cssInnerGutters = feConfig.structure.gutters.inner; | |
// sizesSet is going to be a complete list of breakpoints with null values | |
// that we'll later update to fill in size at bp | |
let sizesSet = {}; | |
// size media query prefixes (except the smallest breakpoint) | |
const breakpointsArr = Object.entries(feConfig.structure.breakpoints).sort((a, b) => parseFloat(a[1]) - parseFloat(b[1])); | |
const mqPrefixes = {}; | |
breakpointsArr.forEach(bp => { | |
let [name, size] = bp; | |
mqPrefixes[name] = size !== '0' ? `(min-width: ${relativeUnits ? remCalc(size) : size})` : ''; | |
sizesSet[name] = null; | |
}); | |
// generate sizes | |
if (sizes !== {}) { | |
// if a string for sizes is passed through | |
if (typeof sizes === 'string') { | |
return sizes; | |
} | |
// if an object of sizes is passed through, convert to an array | |
if (typeof sizes === 'object' && !Array.isArray(sizes)) { | |
// merge the objects | |
sizesSet = Object.assign(sizesSet, sizes); | |
// set up to fill in ALL values for ALL bps | |
const sizesSetKeys = Object.keys(sizesSet); | |
let lastKnownSize; | |
let sizesArr = []; | |
// fill in any missing BP values | |
// if user sends { `lg`: 3 } or { `sm`: 2, `lg`: 3 } | |
// this fills out the missing `sm`, `md`, `xl` values | |
// incase the amount of columns changes per breakpoint | |
// but the column spanning doesn't | |
sizesSetKeys.forEach((bp, index) => { | |
if (sizesSet[bp] === null) { | |
if (index === 0) { | |
sizesSet[bp] = '100vw'; | |
lastKnownSize = '100vw'; | |
} else { | |
sizesSet[bp] = lastKnownSize; | |
} | |
} else { | |
lastKnownSize = sizesSet[bp]; | |
} | |
// calculate size string for bp | |
let bpSizeStr = ''; | |
const sizeAtBreakpoint = getUnitValue(sizesSet[bp]); | |
const cssColumnsAtBreakpoint = cssColumns[bp]; | |
const colWidth = cssContainerWidths[bp] === 'auto' ? 'auto' : parseFloat(cssContainerWidths[bp]); | |
if (typeof sizeAtBreakpoint.value !== 'number') { | |
// no number found, perhaps a `calc()` or something else was passed | |
bpSizeStr = sizeAtBreakpoint.value || '100vw'; | |
} else if (sizeAtBreakpoint.unit) { | |
// has some other unit | |
bpSizeStr = `${sizeAtBreakpoint.value}${sizeAtBreakpoint.unit}`; | |
// px values will be converted to rem later | |
} else if (colWidth !== 'auto') { | |
// calculate based on how much of main col width wide | |
const innerGutter = parseFloat(cssInnerGutters[bp]); | |
let px = (((colWidth - (innerGutter * (cssColumnsAtBreakpoint - 1))) / cssColumnsAtBreakpoint) * sizeAtBreakpoint.value) + ((sizeAtBreakpoint.value - 1) * innerGutter); | |
px = px % 1 !== 0 ? px.toFixed(2) : px; | |
bpSizeStr = `${px}px`; // will be converted to rem later | |
} else { | |
// else calculate one based on %/vw | |
let percent = (sizeAtBreakpoint.value / cssColumnsAtBreakpoint) * 100; | |
percent = percent % 1 !== 0 ? percent.toFixed(2) : percent; | |
bpSizeStr = percent + 'vw'; | |
} | |
sizesSet[bp] = bpSizeStr; | |
}); | |
// don't add sequential duplicate sizes so we have the most minimal output possible | |
let lastSize = -1; | |
sizesSetKeys.forEach(bp => { | |
const size = sizesSet[bp]; | |
if (size !== lastSize) { | |
sizesArr.push({ | |
[`${bp}`]: size | |
}); | |
} | |
lastSize = size; | |
}); | |
// set sizes to the newly made sizes array, so that it can be converted to a string for output below | |
sizes = sizesArr; | |
} | |
// convert array to string and return | |
// NB: if an object was passed, its been converted to an array for final output | |
if (Array.isArray(sizes)) { | |
// make final size string for output | |
let sizesStr = ''; | |
sizes.reverse().forEach((item, index) => { | |
let bp = Object.keys(item)[0]; | |
let size = Object.values(item)[0]; | |
if (relativeUnits && getUnitValue(size).unit === 'px') { | |
size = remCalc(size); | |
} | |
sizesStr += index > 0 ? ', ' : ''; | |
sizesStr += mqPrefixes[bp].length ? mqPrefixes[bp] + ' ' : ''; | |
sizesStr += size; | |
}); | |
return sizesStr; | |
} | |
// catch other entries and do something sensible | |
return JSON.stringify(sizes); | |
} | |
return '100vw'; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// load me in via PHP injection, fetch or bundle in the build process | |
const frontendConfig = { | |
"structure": { | |
"breakpoints": { | |
"sm": "0", | |
"md": "600px", | |
"lg": "900px", | |
"xl": "1200px", | |
"xxl": "1500px", | |
"xxxl": "1944px" | |
}, | |
"columns": { | |
"sm": "4", | |
"md": "8", | |
"lg": "12", | |
"xl": "12", | |
"xxl": "12", | |
"xxxl": "12" | |
}, | |
"container": { | |
"sm": "auto", | |
"md": "auto", | |
"lg": "auto", | |
"xl": "auto", | |
"xxl": "auto", | |
"xxxl": "1800px" | |
}, | |
"gutters": { | |
"inner": { | |
"sm": "20px", | |
"md": "36px", | |
"lg": "36px", | |
"xl": "48px", | |
"xxl": "60px", | |
"xxxl": "60px" | |
}, | |
"outer": { | |
"sm": "24px", | |
"md": "28px", | |
"lg": "48px", | |
"xl": "60px", | |
"xxl": "72px", | |
"xxxl": "0px" | |
} | |
} | |
} | |
// would also contain colors, typography etc. | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* outputs HTML sizes string | |
* @param {object, array, string} sizes - size information to convert | |
* @param {object} feConfig - front end breakpoint, columns | |
* @param {boolean} relativeUnits - convert PX to REM, defaults to true | |
* @returns {string} - for image `sizes` attribute | |
* | |
*/ | |
responsiveImageSizes({ | |
"sm": "100vw", | |
"md": 4, | |
"lg": 7, | |
"xl": "80%", | |
"xxl": "300px" | |
}, feConfig); | |
// '(min-width: 93.75rem) 18.75rem, (min-width: 75rem) 80%, (min-width: 56.25rem) 58.33vw, (min-width: 37.5rem) 50vw, 100vw' | |
// calcs are passed through, PX values inside calc's aren't updated | |
responsiveImageSizes({ | |
"sm": "100vw", | |
"md": 4, | |
"lg": "calc(100vw - 20px)", | |
"xl": "80%", | |
"xxl": "300px" | |
}, feConfig); | |
// '(min-width: 93.75rem) 18.75rem, (min-width: 75rem) 80%, (min-width: 56.25rem) calc(100vw - 20px), (min-width: 37.5rem) 50vw, 100vw' | |
// fills in missing values, 2 column spanning requires different values as the amount of columns changes per breakpoint | |
responsiveImageSizes({ | |
"sm": 2, | |
}, feConfig); | |
// '(min-width: 121.5rem) 15.625rem, (min-width: 56.25rem) 16.67vw, (min-width: 37.5rem) 25vw, 50vw' | |
// to maintain pixel value ouputs | |
responsiveImageSizes({ | |
"sm": "100vw", | |
"md": 4, | |
"lg": 7, | |
"xl": "80%", | |
"xxl": "300px" | |
}, feConfig, false); | |
// '(min-width: 1500px) 300px, (min-width: 1200px) 80%, (min-width: 900px) 58.33vw, (min-width: 600px) 50vw, 100vw' | |
// maintaining pixel value outputs with missing set values | |
responsiveImageSizes({ | |
"sm": 2, | |
}, feConfig, false); | |
// '(min-width: 1944px) 250px, (min-width: 900px) 16.67vw, (min-width: 600px) 25vw, 50vw' | |
// if the smallest breakpoints are set, assumes 100vw until it finds a breapoint setting | |
responsiveImageSizes({ | |
"lg": 2, | |
}, feConfig); | |
// '(min-width: 121.5rem) 15.625rem, (min-width: 56.25rem) 16.67vw, 100vw' | |
// if nothing passed, assumes 100vw | |
responsiveImageSizes({}, feConfig); | |
// '100vw' | |
// if a string is passed instead of an object or array, its returned | |
responsiveImageSizes('100vw', feConfig); | |
// '100vw' | |
// if a string is passed instead of an object or array, its returned (PX values inside calc's aren't updated) | |
responsiveImageSizes('calc(100vw - 20rem)', feConfig); | |
// 'calc(100vw - 20rem)' | |
// if an array of values is passed, its converted to a string (with PX optionally converted to REM) | |
responsiveImageSizes([ | |
{ sm: '100vw' }, | |
{ md: '50vw' }, | |
{ lg: '58.33vw' }, | |
{ xl: '80%' }, | |
{ xxl: '300px' } | |
], feConfig); | |
// '(min-width: 93.75rem) 18.75rem, (min-width: 75rem) 80%, (min-width: 56.25rem) 58.33vw, (min-width: 37.5rem) 50vw, 100vw'; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment