convert css units
i'm trying to get back a style property in all valid 'length' and 'percent' units, converted from the original value set for that property.
e.g., if i have a div with style.width set to 20%, i'd want back an object with that value in percent (of course, 20%), pixels (whatever the actual pixel width is), em, pt, ex, etc.
i realize that 'percentage' is not a 'length' value, and that not all properties that accept length values accept percentage, but want to include that as well.
of course, some values will be dependent on the element specifically, and possibly it's position in the DOM (e.g., getting the em value will require that element's parent computed font size as well).
i can assume that the style is set explicitly for the element - i'm aware of how to retrieve the current computed style of an element - i'm just hoping to not repeat work someone else has probably already done. i'm also aware of http://www.galasoft.ch/myjavascript/WebControls/css-length.html, but it relies on style.pixelWidth or node.clientWidth, and fails in Chrome (I'd assume it fails in Safari as well... and probably others).
i've already got color values worked out (rgb, rgba, hex, name) - this is of course a lot more straightforward. i'm working with values that are mathematically mutable, so really only need '开发者_高级运维length' and 'percent' values (if called on a property set with a non-length, non-percent value - like 'font-size: larger' - the function could fail, or throw an error).
if written procedurally, something like this would be ideal:
function getUnits(target, prop){
var value = // get target's computed style property value
// figure out what unit is being used natively, and it's values - for this e.g., 100px
var units = {};
units.pixel = 100;
units.percent = 50; // e.g., if the prop was height and the parent was 200px tall
units.inch = 1.39; // presumably units.pixel / 72 would work, but i'm not positive
units.point = units.inch / 72;
units.pica = units.point * 12;
// etc...
return units;
}
I'm not asking for someone to write code for me, but my hope is that someone has already done this before and it's available in some open-source library, framework, blog post, tut, whatever. failing that, if someone has a clever idea how to streamline the process, that'd be great as well (the author of the link above created a temporary div and computed a single value to determine the ratios for other units - a handy idea but not one i'm entirely sold on, and definitely one that'd need supplemental logic to handle everything i'm hoping accept).
thanks in advance for any insight or suggestions.
EDIT: updated to allow user to pick a single unit to be returned (e.g., exists as %, get back in px) - big improvement in performance for when that's enough - might end up changing it to just accept a single unit to convert, and get rid the loops. Thanks to eyelidlessness for his help. /EDIT
this is what i've come up with - after preliminary testing it appears to work. i borrowed the temporary div idea from the link mentioned in the original question, but that's about all that was taken from that other class.
if anyone has any input or improvements, i'd be happy to hear it.
(function(){
// pass to string.replace for camel to hyphen
var hyphenate = function(a, b, c){
return b + "-" + c.toLowerCase();
}
// get computed style property
var getStyle = function(target, prop){
if(prop in target.style){ // if it's explicitly assigned, just grab that
if(!!(target.style[prop]) || target.style[prop] === 0){
return target.style[prop];
}
}
if(window.getComputedStyle){ // gecko and webkit
prop = prop.replace(/([a-z])([A-Z])/, hyphenate); // requires hyphenated, not camel
return window.getComputedStyle(target, null).getPropertyValue(prop);
}
if(target.currentStyle){ // ie
return target.currentStyle[prop];
}
return null;
}
// get object with units
var getUnits = function(target, prop, returnUnit){
var baseline = 100; // any number serves
var item; // generic iterator
var map = { // list of all units and their identifying string
pixel : "px",
percent : "%",
inch : "in",
cm : "cm",
mm : "mm",
point : "pt",
pica : "pc",
em : "em",
ex : "ex"
};
var factors = {}; // holds ratios
var units = {}; // holds calculated values
var value = getStyle(target, prop); // get the computed style value
var numeric = value.match(/\d+/); // get the numeric component
if(numeric === null) { // if match returns null, throw error... use === so 0 values are accepted
throw "Invalid property value returned";
}
numeric = numeric[0]; // get the string
var unit = value.match(/\D+$/); // get the existing unit
unit = (unit == null) ? "px" : unit[0]; // if its not set, assume px - otherwise grab string
var activeMap; // a reference to the map key for the existing unit
for(item in map){
if(map[item] == unit){
activeMap = item;
break;
}
}
if(!activeMap) { // if existing unit isn't in the map, throw an error
throw "Unit not found in map";
}
var singleUnit = false; // return object (all units) or string (one unit)?
if(returnUnit && (typeof returnUnit == "string")) { // if user wants only one unit returned, delete other maps
for(item in map){
if(map[item] == returnUnit){
singleUnit = item;
continue;
}
delete map[item];
}
}
var temp = document.createElement("div"); // create temporary element
temp.style.overflow = "hidden"; // in case baseline is set too low
temp.style.visibility = "hidden"; // no need to show it
target.parentNode.appendChild(temp); // insert it into the parent for em and ex
for(item in map){ // set the style for each unit, then calculate it's relative value against the baseline
temp.style.width = baseline + map[item];
factors[item] = baseline / temp.offsetWidth;
}
for(item in map){ // use the ratios figured in the above loop to determine converted values
units[item] = (numeric * (factors[item] * factors[activeMap])) + map[item];
}
target.parentNode.removeChild(temp); // clean up
if(singleUnit !== false){ // if they just want one unit back
return units[singleUnit];
}
return units; // returns the object with converted unit values...
}
// expose
window.getUnits = this.getUnits = getUnits;
})();
tyia
Check out Units, a JavaScript library that does these conversions.
Here's a blog post by the author describing the code.
Late to the party and I don't think this necessarily answers the question fully because I haven't included conversion of percentages. However, I do think it's a good start that can be easily modified for your specific usage.
Javascript function
/**
* Convert absolute CSS numerical values to pixels.
*
* @link https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units#numbers_lengths_and_percentages
*
* @param {string} cssValue
* @param {null|HTMLElement} target Used for relative units.
* @return {*}
*/
window.convertCssUnit = function( cssValue, target ) {
target = target || document.body;
const supportedUnits = {
// Absolute sizes
'px': value => value,
'cm': value => value * 38,
'mm': value => value * 3.8,
'q': value => value * 0.95,
'in': value => value * 96,
'pc': value => value * 16,
'pt': value => value * 1.333333,
// Relative sizes
'rem': value => value * parseFloat( getComputedStyle( document.documentElement ).fontSize ),
'em': value => value * parseFloat( getComputedStyle( target ).fontSize ),
'vw': value => value / 100 * window.innerWidth,
'vh': value => value / 100 * window.innerHeight,
// Times
'ms': value => value,
's': value => value * 1000,
// Angles
'deg': value => value,
'rad': value => value * ( 180 / Math.PI ),
'grad': value => value * ( 180 / 200 ),
'turn': value => value * 360
};
// Match positive and negative numbers including decimals with following unit
const pattern = new RegExp( `^([\-\+]?(?:\\d+(?:\\.\\d+)?))(${ Object.keys( supportedUnits ).join( '|' ) })$`, 'i' );
// If is a match, return example: [ "-2.75rem", "-2.75", "rem" ]
const matches = String.prototype.toString.apply( cssValue ).trim().match( pattern );
if ( matches ) {
const value = Number( matches[ 1 ] );
const unit = matches[ 2 ].toLocaleLowerCase();
// Sanity check, make sure unit conversion function exists
if ( unit in supportedUnits ) {
return supportedUnits[ unit ]( value );
}
}
return cssValue;
};
Example usage
// Convert rem value to pixels
const remExample = convertCssUnit( '2.5rem' );
// Convert time unit (seconds) to milliseconds
const speedExample = convertCssUnit( '2s' );
// Convert angle unit (grad) to degrees
const emExample = convertCssUnit( '200grad' );
// Convert vw value to pixels
const vwExample = convertCssUnit( '80vw' );
// Convert the css variable to pixels
const varExample = convertCssUnit( getComputedStyle( document.body ).getPropertyValue( '--container-width' ) );
// Convert `em` value relative to page element
const emExample = convertCssUnit( '2em', document.getElementById( '#my-element' ) );
Current supported formats
Any format with preceding plus (+
) or minus (-
) symbol is valid, along with any of the following units: px
, cm
, mm
, q
, in
, pc
, pt
, rem
, em
, vw
, vh
, s
, ms
, deg
, rad
, grad
, turn
For example:
10rem
10.2em
-0.34cm
+10.567s
You can see a full combination of formats here: https://jsfiddle.net/thelevicole/k7yt4naw/1/
Émile kind of does this, specifically in its parse
function:
function parse(prop){
var p = parseFloat(prop), q = prop.replace(/^[\-\d\.]+/,'');
return isNaN(p) ? { v: q, f: color, u: ''} : { v: p, f: interpolate, u: q };
}
The prop
argument is the computedStyle for some element. The object that's returned has a v
property (the value), an f
method that is only used later on for animation, and a u
property (the unit of the value, if necessary).
This doesn't entirely answer the question, but it could be a start.
While digging through the SVG spec, I found that SVGLength
provides an interesting DOM API for builtin unit conversion. Here's a function making use of it:
/** Convert a value to a different unit
* @param {number} val - value to convert
* @param {string} from - unit `val`; can be one of: %, em, ex, px, cm, mm, in, pt, pc
* @param {string} to - unit to convert to, same as `from`
* @returns {object} - {number, string} with the number/string forms for the converted value
*/
const convert_units = (() => {
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
const len = rect.width.baseVal;
const modes = {
"%": len.SVG_LENGTHTYPE_PERCENTAGE,
"em": len.SVG_LENGTHTYPE_EMS,
"ex": len.SVG_LENGTHTYPE_EXS,
"px": len.SVG_LENGTHTYPE_PX,
"cm": len.SVG_LENGTHTYPE_CM,
"mm": len.SVG_LENGTHTYPE_MM,
"in": len.SVG_LENGTHTYPE_IN,
"pt": len.SVG_LENGTHTYPE_PT,
"pc": len.SVG_LENGTHTYPE_PC,
};
return (val, from, to, context) => {
if (context)
context.appendChild(rect);
len.newValueSpecifiedUnits(modes[from], val);
len.convertToSpecifiedUnits(modes[to]);
const out = {
number: len.valueInSpecifiedUnits,
string: len.valueAsString
};
if (context)
context.removeChild(rect);
return out;
};
})();
Usage example:
convert_units(1, "in", "mm");
// output: {"number": 25.399999618530273, "string": "25.4mm"}
Some units are relative, so need to be placed in a parent DOM element temporarily to be able to resolve the unit's absolute value. In those cases provide a fourth argument with the parent element:
convert_units(1, "em", "px", document.body);
// output: {"number": 16, "string": "16px"}
精彩评论