Source: css.js

/*!
 * Module CSS
 */

/**
 * @namespace CSS
 */

const MODULE_NAME = 'CSS';



//###[ IMPORTS ]########################################################################################################

import {assert, isNumber, orDefault, isPlainObject, isElement, hasValue, isNaN} from './basic.js';
import {maskForRegEx} from './strings.js';



//###[ EXPORTS ]########################################################################################################

/**
 * @namespace CSS:applyStyles
 */

/**
 * Applies CSS definitions to an HTMLElement, by providing a plain object of property-value-pairs.
 * Properties may be written as default CSS kebab-case properties such as "margin-left" or as JS
 * camel-cased versions such as "marginLeft".
 *
 * Providing a real JS number without a unit will be treated as a pixel value, so defining "'line-height' : 0" will
 * actually result in a 1px line-height. To actually set a unit-less value, just set the value as a string:
 * "'line-height' : '0'".
 *
 * Generally all CSS values are usually strings (this is also the way JS handles this),
 * treating plain numbers as pixels is just a convenience feature, since pixels are most likely to be
 * calculated values, where it is bothersome and error-prone to add the "px" all the time.
 *
 * To remove a property, just set the value to a nullish value. Deleting a property also tries to remove all
 * vendor prefixed variants.
 *
 * This function uses CSSStyleDeclaration.setProperty instead of direct style assignments. This means, that the
 * browser itself decides which value to apply, based on the support of the property. This means, the style object
 * will not be polluted with vendor stuff the browser does not support, but this also means, that all non-standard
 * properties might be refused. If you really need to set something out of spec, use direct style assignment instead.
 *
 * @param {HTMLElement} element - the element to apply the styles to, use null or undefined as value to remove a prop
 * @param {Object} styles - the styles to apply, provided as a plain object, defining property-value-pairs
 * @param {?Boolean} [crossBrowser=false] - set this to true, to automatically generate vendor-prefixed versions of all provided properties
 * @param {?Boolean} [returnCssStyleDeclaration=false] - set this to true, return the CSSStyleDeclaration of the element after the style application, rather than the plain object
 * @throws error if element is not an HTMLElement
 * @throws error if styles is not a plain object
 * @returns {Object|CSSStyleDeclaration} the applied/active styles
 *
 * @memberof CSS:applyStyles
 * @alias applyStyles
 * @example
 * applyStyles(document.body, {backgroundColor : red, transition : 'all 200ms'}, true);
 * applyStyles(document.querySelector('main'), {'font-family' : 'serif'}, false, true);
 */
export function applyStyles(element, styles, crossBrowser=false, returnCssStyleDeclaration=false){
	const __methodName__ = 'applyStyles';

	crossBrowser = orDefault(crossBrowser, false, 'bool');
	returnCssStyleDeclaration = orDefault(returnCssStyleDeclaration, false, 'bool');

	assert(isElement(element), `${MODULE_NAME}:${__methodName__} | element is not an html element`);
	assert(isPlainObject(styles), `${MODULE_NAME}:${__methodName__} | styles must be a plain object`);

	const vendorPrefixes = ['-webkit-', '-moz-', '-ms-', '-o-', '-khtml-'];

	if( crossBrowser ){
		Object.entries({...styles}).forEach(([cssKey, cssValue]) => {
			vendorPrefixes.forEach(vendorPrefix => {
				if(cssKey === 'transition'){
					styles[vendorPrefix+cssKey] = cssValue.replace('transform', `${vendorPrefix}transform`);
				} else {
					styles[vendorPrefix+cssKey] = cssValue;
				}
			});
		});
	}

	Object.entries({...styles}).forEach(([cssKey, cssValue]) => {
		if( isNumber(cssValue) && (cssValue !== 0) ){
			styles[cssKey] = `${cssValue}px`;
			element.style.setProperty(cssKey, styles[cssKey]);
		} else if( !hasValue(cssValue) ){
			vendorPrefixes.forEach(vendorPrefix => {
				delete styles[vendorPrefix+cssKey];
				element.style.removeProperty(vendorPrefix+cssKey);
			});
			delete styles[cssKey];
			element.style.removeProperty(cssKey);
		} else {
			styles[cssKey] = `${cssValue}`;
			element.style.setProperty(cssKey, styles[cssKey]);
		}
	});

	return returnCssStyleDeclaration ? element.style : styles;
}



/**
 * @namespace CSS:cssValueToNumber
 */

/**
 * Converts a CSS-value to a number without unit. If the base number is an integer the result will also
 * be an integer, float values will also be converted correctly.
 *
 * @param {String} value - the css-value to convert
 * @returns {Number|NaN} true number representation of the given value or NaN if the value is not parsable
 *
 * @memberof CSS:cssValueToNumber
 * @alias cssValueToNumber
 * @example
 * document.querySelector('main').style.setProperty('width', '99vh');
 * cssValueToNumber(document.querySelector('main').style.getPropertyValue('width'));
 * => 99
 */
export function cssValueToNumber(value){
	return parseFloat(orDefault(value, '', 'str'));
}



/**
 * @namespace CSS:cssUrlValueToUrl
 */

/**
 * Converts a CSS-URL-value ("url('/foo/bar/baz.jpg')") to a plain URL usable in requests or src-attributes.
 *
 * @param {String} urlValue - the URL-value from CSS
 * @param {?String} [baseUrl=null] - if you want to transform the URL by substituting the start of the path or URL with something fitting for your context, define what to replace here
 * @param {?String} [baseUrlSubstitution=null] - if you want to transform the URL by substituting the start of the path or URL with something fitting for your context, define what to replace the baseUrl with here
 * @returns {String|Array<String>|null} the extracted URL (or list of URLs if value contained several) with substitutions (if defined) or null if no URL-values were found
 *
 * @memberof CSS:cssUrlValueToUrl
 * @alias cssUrlValueToUrl
 * @example
 * cssUrlValueToUrl('url("https://foobar.com/test.jpg")', 'https://foobar.com', '..');
 * => '../test.jpg'
 * cssUrlValueToUrl(`url(/foo/bar),
 * url('https://google.de') url("test.jpg"),url(omg.svg)
 * url(http://lol.com)`)
 * => ['/foo/bar', 'https://google.com', 'test.jpg', 'omg.svg', 'http://lol.com']
 */
export function cssUrlValueToUrl(urlValue, baseUrl=null, baseUrlSubstitution=null){
	urlValue = orDefault(urlValue, '', 'str');
	baseUrl = orDefault(baseUrl, null, 'str');
	baseUrlSubstitution = orDefault(baseUrlSubstitution, null, 'str');

	const
		urlValueRex = new RegExp('(?:^|\\s|,)url\\((?:\'|")?([^\'"\\n\\r\\t]+)(?:\'|")?\\)', 'gmi'),
		matches = []
	;

	let match;
	while( (match = urlValueRex.exec(urlValue)) !== null ){
		match = match[1];
		if( hasValue(baseUrl, baseUrlSubstitution) ){
			match = match.replace(new RegExp(`^${maskForRegEx(baseUrl)}`), baseUrlSubstitution);
		}
		matches.push(match);
	}

	if( matches.length === 1 ){
		return matches[0];
	} else if( matches.length > 1 ){
		return matches;
	} else {
		return null;
	}
}



/**
 * @namespace CSS:remByPx
 */

/**
 * Calculates a rem value based on a given px value.
 * As a default this method takes the font-size (supposedly being in px) of the html-container.
 * You can overwrite this behaviour by setting initial to a number to use as a base px value or
 * to a string, which then defines a new selector for an element to get the initial font-size from.
 * You can also provide an HTMLElement directly, but keep in mind that the element's font size definition
 * has to be in pixels, to make this work.
 *
 * In most cases you will have to define the initial value via a constant or a selector to a container
 * with non-changing font-size, since you can never be sure which relative font-size applies atm, even on first
 * call, after dom ready, since responsive definitions might already be active, returning a viewport-specific
 * size.
 *
 * @param  {Number} px - the pixel value to convert to rem
 * @param  {?(Number|String|HTMLElement)} [initial='html'] - either a pixel value to use as a conversion base, a selector for an element to get the initial font-size from or the element itself; keep in mind, that the element's font-size definition has to be in px
 * @throws error if given selector in initial does not return an element
 * @returns {String|null} the rem value string to use in a css definition or null if the value could not be calculated
 *
 * @memberof CSS:remByPx
 * @alias remByPx
 * @example
 * remByPx(20, 16);
 * => '1.25rem'
 * remByPx('100px', 'p.has-base-fontsize');
 */
export function remByPx(px, initial='html'){
	px = cssValueToNumber(px);
	initial = orDefault(initial, 'html');

	if( isElement(initial) ){
		initial = cssValueToNumber(initial.style.getPropertyValue('font-size'));
	} else {
		const value = cssValueToNumber(initial);
		if( isNaN(value) ){
			const element = document.querySelector(initial);
			assert(hasValue(element), `${MODULE_NAME}:remByPx | selector does not return element`);
			initial = cssValueToNumber(element.style.getPropertyValue('font-size'));
		} else {
			initial = value;
		}
	}

	const remVal = px / initial;

	if( (initial !== 0) && !isNaN(remVal) ){
		return `${remVal}rem`;
	} else {
		return null;
	}
}