rfs
Version:
Powerful & easy-to-use responsive resizing engine.
159 lines (128 loc) • 5.36 kB
JavaScript
'use strict';
const postcss = require('postcss');
const valueParser = require('postcss-value-parser');
const defaultOptions = {
baseValue: 20,
unit: 'rem',
breakpoint: 1200,
breakpointUnit: 'px',
factor: 10,
twoDimensional: false,
unitPrecision: 5,
remValue: 16,
functionName: 'rfs',
enableRfs: true,
mode: 'min-media-query'
};
module.exports = class {
constructor(opts) {
this.opts = { ...defaultOptions, ...opts };
if (typeof this.opts.baseValue !== 'number') {
if (this.opts.baseValue.endsWith('px')) {
this.opts.baseValue = Number.parseFloat(this.opts.baseValue);
} else if (this.opts.baseValue.endsWith('rem')) {
this.opts.baseValue = Number.parseFloat(this.opts.baseValue) * this.opts.remValue;
} else {
throw new TypeError('`baseValue` option is invalid, it should be set in `px` or `rem`.');
}
}
if (typeof this.opts.breakpoint !== 'number') {
if (this.opts.breakpoint.endsWith('px')) {
this.opts.breakpoint = Number.parseFloat(this.opts.breakpoint);
} else if (this.opts.breakpoint.endsWith('em')) {
this.opts.breakpoint = Number.parseFloat(this.opts.breakpoint) * this.opts.remValue;
} else {
throw new TypeError('`breakpoint` option is invalid, it should be set in `px`, `rem` or `em`.');
}
}
if (!['px', 'rem', 'em'].includes(this.opts.breakpointUnit)) {
throw new TypeError('`breakpointUnit` option is invalid, it should be `px`, `rem` or `em`.');
}
}
toFixed(number, precision) {
const multiplier = 10 ** (precision + 1);
const wholeNumber = Math.floor(number * multiplier);
return Math.round(wholeNumber / 10) * 10 / multiplier;
}
renderValue(value) {
// Do not add unit if value is 0
if (value === 0) {
return value;
}
// Render value in desired unit
return this.opts.unit === 'rem' ?
`${this.toFixed(value / this.opts.remValue, this.opts.unitPrecision)}rem` :
`${this.toFixed(value, this.opts.unitPrecision)}px`;
}
process(declarationValue, fluid) {
const parsed = valueParser(declarationValue);
// Function walk() will visit all the of the nodes in the tree,
// invoking the callback for each.
parsed.walk(node => {
// Since we only want to transform rfs() values,
// we can ignore everything else.
if (node.type !== 'function' && node.value !== this.opts.functionName) {
return;
}
const wordNodes = node.nodes.filter(node => node.type === 'word');
for (const node of wordNodes) {
node.value = node.value.replace(/^(-?\d*\.?\d+)(.*)/g, (match, value, unit) => {
value = Number.parseFloat(value);
// Return value if it's not a number or px/rem value
if (Number.isNaN(value) || !['px', 'rem'].includes(unit)) {
return match;
}
// Convert to px if in rem
if (unit === 'rem') {
value *= this.opts.remValue;
}
// Only add responsive function if needed
if (!fluid || this.opts.baseValue >= Math.abs(value) || this.opts.factor <= 1 || !this.opts.enableRfs) {
return this.renderValue(value);
}
// Calculate base and difference
let baseValue = this.opts.baseValue + ((Math.abs(value) - this.opts.baseValue) / this.opts.factor);
const diff = Math.abs(value) - baseValue;
// Divide by remValue if needed
if (this.opts.unit === 'rem') {
baseValue /= this.opts.remValue;
}
const viewportUnit = this.opts.twoDimensional ? 'vmin' : 'vw';
return value > 0 ?
`calc(${this.toFixed(baseValue, this.opts.unitPrecision)}${this.opts.unit} + ${this.toFixed(diff * 100 / this.opts.breakpoint, this.opts.unitPrecision)}${viewportUnit})` :
`calc(-${this.toFixed(baseValue, this.opts.unitPrecision)}${this.opts.unit} - ${this.toFixed(diff * 100 / this.opts.breakpoint, this.opts.unitPrecision)}${viewportUnit})`;
});
}
// Now we will transform the existing rgba() function node
// into a word node with the hex value
node.type = 'word';
node.value = valueParser.stringify(node.nodes);
});
return parsed.toString();
}
// Return the value without `rfs()` function
// eg. `4px rfs(32px)` => `.25rem 2rem`
value(value) {
return this.process(value, false);
}
// Convert `rfs()` function to fluid css
// eg. `4px rfs(32px)` => `.25rem calc(1.325rem + 0.9vw)`
fluidValue(value) {
return this.process(value, true);
}
renderMediaQuery() {
const mediaQuery = {
name: 'media'
};
const dimPrefix = this.opts.mode === 'min-media-query' ? 'min' : 'max';
const dimConnector = this.opts.mode === 'min-media-query' ? ' and' : ',';
const breakpoint = this.opts.breakpointUnit === 'px' ? this.opts.breakpoint : this.opts.breakpoint / this.opts.remValue;
mediaQuery.params = this.opts.twoDimensional ?
`(${dimPrefix}-width: ${breakpoint}${this.opts.breakpointUnit})${dimConnector} (${dimPrefix}-height: ${breakpoint}${this.opts.breakpointUnit})` :
`(${dimPrefix}-width: ${breakpoint}${this.opts.breakpointUnit})`;
return postcss.atRule(mediaQuery);
}
getOptions() {
return this.opts;
}
};