开发者

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"}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜