将CSS单位转换

13
我正在尝试获取所有有效的“长度”和“百分比”单位中设置的样式属性,并转换为原始值。
例如,如果我有一个样式宽度设置为20%的div,我希望返回一个包含该值的对象,以百分比表示(当然是20%),像素(实际像素宽度是多少),em,pt,ex等等。
我意识到“百分比”不是“长度”值,并且并非所有接受长度值的属性都接受百分比,但我也想包括在内。
当然,一些值将取决于特定元素,可能还要考虑它在DOM中的位置(例如,获取em值将需要该元素的父计算字体大小)。
我可以假设样式是明确设置给元素的 - 我知道如何检索元素的当前计算样式 - 我只是希望不重复别人已经完成的工作。我也知道http://www.galasoft.ch/myjavascript/WebControls/css-length.html,但它依赖于style.pixelWidth或node.clientWidth,并在Chrome中失败(我认为它在Safari中也会失败... 可能还有其他浏览器)。
我已经解决了颜色值(rgb,rgba,hex,name) - 当然这更加直接。我正在处理数学可变的值,因此只需要“长度”和“百分比”值(如果调用设置为非长度,非百分比值的属性(如“font-size:larger”)的属性,则该函数可能会失败或引发错误)。
如果按照过程式编写,类似这样的代码会是理想的:
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;
}

我并不要求有人为我编写代码,但我的希望是已经有人做过这件事,并且它可以在某些开源库、框架、博客文章、教程等中找到。如果没有的话,如果有人有一个聪明的想法来简化这个过程,那也很好(上面链接的作者创建了一个临时div并计算单个值来确定其他单位的比率——一个方便的想法,但我并不完全赞同,并且肯定需要补充逻辑来处理我希望接受的所有内容)。
提前感谢您的任何见解或建议。

刚刚删除了我的答案;应该先完整地阅读你的问题! - Kai
5个回答

8

编辑:更新后,允许用户选择返回单个单位(例如以%表示,返回px)- 当足够时,性能大幅提升 - 可能最终会更改为仅接受单个要转换的单位,并消除循环。感谢eyelidlessness的帮助。/编辑

这就是我想到的 - 经过初步测试,似乎可以工作。我从原始问题中提到的链接中借鉴了临时div的想法,但那几乎是从其他类中获取的全部内容。

如果有人有任何意见或改进意见,我很乐意听取。

   (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


如果您交换map中的键/值并使用var activeMap = unit in map ? map[unit] : null;,您可能会获得更好的性能。否则,很棒!+1 - eyelidlessness
好的观点 - 我想到在另外两个循环中进行额外的查找可能会抵消跳过activeMap循环所获得的任何收益... units[item]和factors[item]与units[map[item]]和factors[map[item]]之间的区别...但我会尝试一下。目前,当样式没有被明确/内联设置时,每次调用需要大约1毫秒(这是在一台2年前的中档笔记本电脑上),因此它可以从任何我可以减少的地方受益。虽然我怀疑主要是DOM插入和检索计算样式,而这两者都是不可避免的。感谢您的回复和建议。 - momo
1
我在空腹喝酒时不应该发表评论!我没有注意到其他循环依赖于那个对象。在这种情况下,最好同时保留该对象和一个名称/值倒置的对象。关于计算时间,1毫秒已经非常低了,但你不是在JavaScript内部计时吧?你可能永远无法得到低于1毫秒的时间,因为这是该语言中的时间粒度。顺便说一下,我碰巧又回到这里,通常人们不会注意到回复,除非你以@username(此时SO会通知)开头。 - eyelidlessness
@eyelidlessness,很高兴了解到关于@username的信息!我之前不知道。关于时间 - 我没有计算单个调用 - 我运行了500和1000次循环几次(500平均约400毫秒,1000平均约800毫秒)。如果针对选择器引擎返回的集合运行,这有点重。我正在考虑缓存因子对象并在相同的目标/父级被使用时进行回收,但如果父级已更改其自身尺寸或字体大小,则会产生错误的结果,例如em、ex和尺寸百分比。我认为它可能只能接受它是重负载的。 - momo

6

我来晚了,我认为这并不完全回答了问题,因为我没有包括百分比的转换。然而,我认为这是一个很好的开始,可以轻松地修改以适合您特定的用途。

Javascript函数

/**
 * 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;

};

示例用法

// 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' ) );

目前支持的格式

以加号(+)或减号(-)开头的任何格式都是有效的,此外还可以使用以下单位:pxcmmmqinpcptrememvwvhsmsdegradgradturn

例如:

10rem
10.2em
-0.34cm
+10.567s

你可以在这里看到所有格式的完整组合:https://jsfiddle.net/thelevicole/k7yt4naw/1/

3

请查看Units,这是一个能够进行这些转换的JavaScript库。

这篇博文是作者描述代码的文章。


0

Émile 在其 parse 函数中实现了这种功能:

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 };
}

prop参数是某个元素的computedStyle。返回的对象具有v属性(值),一个f方法,仅在稍后用于动画,以及一个u属性(如果需要,表示值的单位)。

这并不完全回答问题,但可以作为一个开端。


谢谢回复 - 那部分很简单,我正在寻找执行转换本身的逻辑... - momo

0
在研究SVG规范时,我发现SVGLength提供了一个有趣的DOM API,用于内置单位转换。下面是一个利用它的函数:
/** 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;
    };
})();

使用示例:

convert_units(1, "in", "mm");
// output: {"number": 25.399999618530273, "string": "25.4mm"}

有些单位是相对的,因此需要将它们暂时放置在父DOM元素中,以便能够解析单位的绝对值。在这些情况下,提供第四个参数并指定父元素:

convert_units(1, "em", "px", document.body);
// output: {"number": 16, "string": "16px"}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接