bootstrap-js-buttons
Version:
Create Bootstrap buttons using CSS variables via Javascript
438 lines (368 loc) • 14.8 kB
JavaScript
var DEBUG = false;
function debug() {
DEBUG = !DEBUG;
}
function _color(value) {
value = value.trim();
if (value.charAt(0) === '#') {
return value.substring(1);
}
return value;
}
function red(value) {
return parseInt(_color(value).substring(0, 2), 16);
}
function green(value) {
return parseInt(_color(value).substring(2, 4), 16);
}
function blue(value) {
return parseInt(_color(value).substring(4, 6), 16);
}
function rgba(value, a) {
a = a * 100;
return `rgb(${ red(value) } ${ green(value) } ${ blue(value) } / ${ a }%)`;
}
function mix(color_1, color_2, weight) {
color_1 = _color(color_1);
color_2 = _color(color_2);
function d2h(d) {
return d.toString(16);
} // convert a decimal value to hex
function h2d(h) {
return parseInt(h, 16);
} // convert a hex value to decimal
weight = (typeof (weight) !== 'undefined') ? weight : 50; // set the weight to 50%, if that argument is omitted
var color = "#";
for (var i = 0; i <= 5; i += 2) { // loop through each of the 3 hex pairs—red, green, and blue
var v1 = h2d(color_1.substr(i, 2)), // extract the current pairs
v2 = h2d(color_2.substr(i, 2)),
// combine the current pairs from each source color, according to the specified weight
val = d2h(Math.floor(v2 + (v1 - v2) * (weight / 100.0)));
while (val.length < 2) {
val = '0' + val;
} // prepend a '0' if val results in a single digit
color += val; // concatenate val to our new color string
}
return color;
}
// Tint a color: mix a color with white
function tint(color, weight) {
return mix('ffffff', color, weight);
}
// Shade a color: mix a color with black
function shade(color, weight) {
return mix('000000', color, weight);
}
// Return opaque color
// opaque(#fff, rgba(0, 0, 0, .5)) => #808080
function opaque(background, foreground) {
// mix(rgba($foreground, 1), $background, opacity($foreground) * 100);
return mix(foreground, background, opacity(foreground) * 100);
}
// A list of pre-calculated numbers of pow(($value / 255 + .055) / 1.055, 2.4). (from 0 to 255)
// stylelint-disable-next-line scss/dollar-variable-default, scss/dollar-variable-pattern
const _luminanceList = [
.0008, .0010, .0011, .0013, .0015, .0017, .0020, .0022, .0025, .0027, .0030, .0033, .0037, .0040, .0044, .0048,
.0052, .0056, .0060, .0065, .0070, .0075, .0080, .0086, .0091, .0097, .0103, .0110, .0116, .0123, .0130, .0137,
.0144, .0152, .0160, .0168, .0176, .0185, .0194, .0203, .0212, .0222, .0232, .0242, .0252, .0262, .0273, .0284,
.0296, .0307, .0319, .0331, .0343, .0356, .0369, .0382, .0395, .0409, .0423, .0437, .0452, .0467, .0482, .0497,
.0513, .0529, .0545, .0561, .0578, .0595, .0612, .0630, .0648, .0666, .0685, .0704, .0723, .0742, .0762, .0782,
.0802, .0823, .0844, .0865, .0887, .0908, .0931, .0953, .0976, .0999, .1022, .1046, .1070, .1095, .1119, .1144,
.1170, .1195, .1221, .1248, .1274, .1301, .1329, .1356, .1384, .1413, .1441, .1470, .1500, .1529, .1559, .1590,
.1620, .1651, .1683, .1714, .1746, .1779, .1812, .1845, .1878, .1912, .1946, .1981, .2016, .2051, .2086, .2122,
.2159, .2195, .2232, .2270, .2307, .2346, .2384, .2423, .2462, .2502, .2542, .2582, .2623, .2664, .2705, .2747,
.2789, .2831, .2874, .2918, .2961, .3005, .3050, .3095, .3140, .3185, .3231, .3278, .3325, .3372, .3419, .3467,
.3515, .3564, .3613, .3663, .3712, .3763, .3813, .3864, .3916, .3968, .4020, .4072, .4125, .4179, .4233, .4287,
.4342, .4397, .4452, .4508, .4564, .4621, .4678, .4735, .4793, .4851, .4910, .4969, .5029, .5089, .5149, .5210,
.5271, .5333, .5395, .5457, .5520, .5583, .5647, .5711, .5776, .5841, .5906, .5972, .6038, .6105, .6172, .6240,
.6308, .6376, .6445, .6514, .6584, .6654, .6724, .6795, .6867, .6939, .7011, .7084, .7157, .7231, .7305, .7379,
.7454, .7529, .7605, .7682, .7758, .7835, .7913, .7991, .8070, .8148, .8228, .8308, .8388, .8469, .8550, .8632,
.8714, .8796, .8879, .8963, .9047, .9131, .9216, .9301, .9387, .9473, .9560, .9647, .9734, .9823, .9911, 1
];
// Return WCAG2.0 relative luminance
// See https://www.w3.org/WAI/GL/wiki/Relative_luminance
// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
function luminance(color) {
let rgb = {
r: red(color),
g: green(color),
b: blue(color)
};
for (let [name, value] of Object.entries(rgb)) {
if (value / 255 < .03928) {
value = value / 255 / 12.92;
} else {
value = _luminanceList[value];
}
rgb[name] = value;
}
return +(rgb.r * .2126 + rgb.g * .7152 + rgb.b * .0722).toFixed(8);
}
function contrastRatio(background, options) {
options.colorContrastLight = options.colorContrastLight || '#ffffff';
options.foreground = options.foreground || options.colorContrastLight;
const l1 = luminance(background);
// If we don't allow opacity in the foreground color this will alway equal 'foreground'
// $l2: luminance(opaque($background, $foreground));
const l2 = luminance(options.foreground);
if (l1 > l2) {
return +((l1 + .05) / (l2 + .05)).toFixed(10);
}
return +((l2 + .05) / (l1 + .05)).toFixed(10);
}
// https://getbootstrap.com/docs/5.0/customize/sass/#color-contrast
function colorContrast(background, options) {
options = options || {};
options.colorContrastDark = options.colorContrastDark || '#000000';
options.colorContrastLight = options.colorContrastLight || '#ffffff';
options.minContrastRatio = options.minContrastRatio || 4.5;
const foregrounds = [options.colorContrastLight, options.colorContrastDark, '#ffffff', '#000000'];
let maxRatio = 0;
let maxRatioColor = null;
for (const color of foregrounds) {
let ratio = contrastRatio(background, {
foreground: color,
colorContrastLight: options.colorContrastLight,
colorContrastDark: options.colorContrastDark
});
if (ratio > options.minContrastRatio) {
return color;
} else if (ratio > maxRatio) {
maxRatio = ratio;
maxRatioColor = color;
}
}
if (DEBUG) {
console.log(`Found no color leading to ${options.minContrastRatio}:1 contrast ratio against ${background}...`);
}
return maxRatioColor;
}
function addStyleElement(name, css) {
var head = document.querySelector('head');
var styleId = 'bs' + name;
var oldStyle = document.getElementById(styleId);
var style = document.createElement('style');
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
style.setAttribute('id', styleId);
if (oldStyle) {
head.replaceChild(style, oldStyle);
} else {
head.appendChild(style);
}
}
function squashCss(css) {
return css.trim().replace(/\s+/g, ' ');
}
function gradientBackground(color, options) {
options.enableGradients = options.enableGradients || false;
let css = `
background-color: ${color};
`
if (options.enableGradients) {
css += `
background-image: var(--bs-gradient);
`
}
return css;
}
function btnBoxShadow(options) {
options = options || {};
options.enableShadows = options.enableShadows || false;
options.btnBoxShadow = options.btnBoxShadow || 'inset 0 1px 0 #ffffff, 0 1px 1px #000000';
// FIXME: user is responsible for using 'none' correctly.
if (options.enableShadows) {
return `box-shadow: ${options.btnBoxShadow}`
}
return '';
}
function focusBoxShadow(shadow, color, border, options) {
options = options || {};
options.btnFocusWidth = options.btnFocusWidth || '.25rem';
if (options.enableShadows) {
return `box-shadow: ${shadow} 0 0 0 ${options.btnFocusWidth} ${rgba(mix(color, border, 15), .5)}`;
} else {
return `box-shadow: 0 0 0 ${options.btnFocusWidth} ${rgba(mix(color, border, 15), .5)}`;
}
}
function focusBoxShadowOutline(shadow, color, options) {
options = options || {};
options.btnFocusWidth = options.btnFocusWidth || '.25rem';
if (options.enableShadows) {
return `box-shadow: ${shadow} 0 0 0 ${options.btnFocusWidth} ${rgba(color, .5)}`;
} else {
return `box-shadow: 0 0 0 ${options.btnFocusWidth} ${rgba(color, .5)}`;
}
}
function removeGradientIfEnabled(options) {
options = options || {};
if (options.enableGradients) {
return `background-image: none;`
}
return '';
}
function defaultOptions(options) {
options = options || {};
options.colorContrastLight = options.colorContrastLight || '#ffffff';
options.colorContrastDark = options.colorContrastDark || '#000000';
options.minContrastRatio = options.minContrastRatio || 4.5;
options.btnHoverBgShadeAmount = options.btnHoverBgShadeAmount || 15;
options.btnHoverBgTintAmount = options.btnHoverBgTintAmount || 15;
options.btnHoverBorderShadeAmount = options.btnHoverBorderShadeAmount || 20;
options.btnHoverBorderTintAmount = options.btnHoverBorderTintAmount || 10;
options.btnActiveBgShadeAmount = options.btnHoverBgShadeAmount || 20;
options.btnActiveBgTintAmount = options.btnActiveBgTintAmount || 20;
options.btnActiveBorderShadeAmount = options.btnActiveBorderShadeAmount || 25;
options.btnActiveBorderTintAmount = options.btnActiveBorderTintAmount || 10;
options.btnBoxShadow = options.btnBoxShadow || `inset 0 1px 0 ${rgba('#ffffff', .15)}, 0 1px 1px ${rgba('#000000', .075)} !default`;
options.btnActiveBoxShadow = options.btnActiveBoxShadow || `inset 0 3px 5px ${rgba('#000000', .125)}`;
options.enableGradients = options.enableGradients || false;
options.enableShadows = options.enableShadows || false;
return options;
}
function buttonVariant(
name,
background,
border,
options
) {
if (name.charAt(0) !== '.') {
name = '.' + name;
}
options = defaultOptions(options);
/* Function */
options.color = options.color || colorContrast(background, options);
border = border ? boarder : background;
if (!options.hoverBackground) {
if (options.color === options.colorContrastLight) {
options.hoverBackground = shade(background, options.btnHoverBgShadeAmount);
} else {
options.hoverBackground = tint(background, options.btnHoverBgTintAmount);
}
}
if (!options.hoverBorder) {
if (options.color === options.colorContrastLight) {
options.hoverBorder = shade(border, options.btnHoverBorderShadeAmount);
} else {
options.hoverBorder = tint(border, options.btnHoverBgTintAmount);
}
}
options.hoverColor = options.hoverColor || colorContrast(options.hoverBackground, options);
if (!options.activeBackground) {
if (options.color === options.colorContrastLight) {
options.activeBackground = shade(background, options.btnActiveBgShadeAmount);
} else {
options.activeBackground = tint(background, options.btnActiveBorderTintAmount);
}
}
if (!options.activeBorder) {
if (options.color === options.colorContrastLight) {
options.activeBorder = shade(border, options.btnActiveBorderShadeAmount);
} else {
options.activeBorder = tint(border, options.btnActiveBorderTintAmount);
}
}
options.activeColor = options.activeColor || colorContrast(options.activeBackground, options);
options.disabledBackground = background;
options.disabledBorder = border;
options.disabledColor = colorContrast(options.disabledBackground);
//---
let css = `
${name} {
color: ${options.color};
${gradientBackground(background, options)}
border-color: ${border};
${btnBoxShadow(options)}
}
${name}:hover {
color: ${options.hoverColor};
${gradientBackground(options.hoverBackground, options)};
border-color: ${options.hoverBorder};
}
.btn-check:focus + ${name},
${name}:focus {
color: ${options.hoverColor};
${gradientBackground(options.hoverBackground, options)};
border-color: ${options.hoverBorder};
${focusBoxShadow(options.btnBoxShadow, options.color, border, options)};
}
.btn-check:checked + ${name},
.btn-check:active + ${name},
${name}:active,
${name}.active,
.show > ${name}.dropdown-toggle {
color: ${options.activeColor};
background-color: ${options.activeBackground};
${removeGradientIfEnabled(options)}
border-color: ${options.activeBorder};
}
${name}:focus {
${focusBoxShadow(options.activeBoxShadow, options.color, border, options)};
}
${name}:disabled,
${name}.disabled {
color: ${options.disabledColor};
background-color: ${options.disabledBackground};
${removeGradientIfEnabled(options)}
border-color: ${options.disabledBorder};
}`;
//---
addStyleElement(name, squashCss(css));
}
function buttonOutlineVariant(
name,
color,
options
) {
if (name.charAt(0) !== '.') {
name = '.' + name;
}
options = defaultOptions(options);
/* Function */
options.colorHover = options.colorHover || colorContrast(color, options);
options.activeBackground = options.activeBackground || color;
options.activeBorder = options.activeBorder || color;
options.activeColor = options.activeColor || options.activeBackground;
//---
let css = `
${name} {
color: ${color};
border-color: ${color};
}
${name}:hover {
color: ${options.colorHover};
background-color: ${options.activeBackground};
border-color: ${options.activeBorder};
}
.btn-check:focus + ${name},
${name}:focus {
${focusBoxShadowOutline(options.btnBoxShadow, color, options)};
}
.btn-check:checked + ${name},
.btn-check:active + ${name},
${name}:active,
${name}.active,
${name}.dropdown-toggle {
color: ${color};
background-color: ${options.activeBackground};
border-color: ${options.activeBorder};
}
${name}:focus {
${focusBoxShadowOutline(options.activeBoxShadow, color, options)};
}
${name}:disabled,
${name}.disabled {
color: ${color};
background-color: transparent;
}`;
//---
addStyleElement(name, squashCss(css));
}
export {
buttonVariant,
buttonOutlineVariant,
debug
}