踏歌

css变量之条件语句

踏歌 · 2016-12-25翻译 · 636阅读 原文链接

本文原文地址http://kizu.ru/en/fun/conditions-for-css-variables/ 如有翻译不当之处,请指出。

我将从这个开始:规范中没有(有一个css条件规则的模块,但是别期待它能覆盖css变量-它涵盖了一些规则的东西。这里还有一个关于@when/@else的原则,同样,它和变量没有任何共同点)CSS变量的使用场景。我认为这是规则中的一个很大的缺陷,然而变量提供了很多过去不可能打东西。对于使用场景的缺失实在是令人沮丧,我们可能会在很多地方用到它们。

如果现在我们要为css变量虚构一些条件语句?很好,和许多其他CSS中的东西一样,我们可以在相同的情况下进行hack。

问题的定义

我们需要使用一个CSS变量为不同的值设置不同的CSS的属性,但不直接基于此变量(也就是说,这些值不应该基于我们的变量计算而来)。因此我们需要条件

使用二元条件的计算

长话短话,我现在就提出解决方案给你,稍后解释一下:

:root {
    --is-big: 0;
}

.is-big {
    --is-big: 1;
}

.block {
    padding: calc(
        25px * var(--is-big) +
        10px * (1 - var(--is-big))
    );
    border-width: calc(
        3px * var(--is-big) +
        1px * (1 - var(--is-big))
    );
}

在这个例子中,我们给所有带有.block的元素都加上了10px的内边距和1px的边框,除非这些元素上的--is-big变量不为1,在这种情况下,它们将分别变为25px3px. 理解这一点的机制非常简单:我们在使用calc()的单个计算中使用我们的可能值,我们使其中一个值无效,并且基于变量的值得到另一个值,该变量的值可以是10。换句话说,我们有25px * 1 + 10px * 025px * 0 + 10px * 1两种情况。

更复杂的条件

我们不仅可以使用这个方法在两个可能的值中选择,还可以从3个或者更多的值中选择。然而,对于每增加一个可能的值,计算将变得更加复杂。为了3个可能的值中进行选择,计算将看起来像这样:

.block {
    padding: calc(
        100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +
         20px * var(--foo) * (2 - var(--foo)) +
          3px * var(--foo) * (1 - var(--foo)) * -0.5
    );
}

--foo变量可以接受0,1,和2,相应计算出来的内边距为100px,20px或者3px`。

原理是一样的:我们只需要将每个可能的值乘以一个表达式,当该值的条件是我们需要的值时,该值等于1,在其他情况下为0。这个表达式可以很容易的组成:我们只需要使我们的条件变量的每个其他可能的值无效,这样做后,我们需要在那里添加触发值,看看是否需要调整结果,使其等于1。

规则中可能存在陷阱

随着这种计算的复杂性的增加,达到一定程度后,它们便会失效。为什么?在规则中有这样的说明:

用户代理必须支持至少20个术语的calc()表达式,其中每个NUMBER,DIMENSION或PERCENTAGE是一个术语。如果calc()表达式包含的术语数超过支持的数量,则将其视为无效。

当然,我测试了这一点然而并没有在我测试的浏览器中发现这样的限制,但是当你会写一些真正复杂的代码时可能会遇到这样的限制,或者一些浏览器可能在未来会引入这个限制,所以在使用真正复杂的计算时要小心。

颜色的条件

正如你所看到的,这些计算仅适用于你可以计算的东西。因此你不可能用它来切换display属性的值亦或者是其他非数字的属性。但是颜色是否可以用它呢?事实上,我们可以计算出各个颜色的成分。可悲的是,现在它只能在Webkits和Blinks工作,因为Firefox还不支持raga()和颜色方法中使用calc().

但是当支持的时候(或者你想在已经支持的浏览器中进行实验),我们可能会像下面这样做:

:root {
    --is-red: 0;
}

.block {
    background: rgba(
        calc(
            255*var(--is-red) +
            0*(1 - var(--is-red))
            ),
        calc(
            0*var(--is-red) +
            255*(1 - var(--is-red))
            ),
        0, 1);
}

我们的颜色默认是石灰色,如果--is-red被赋值为1,颜色会变为红色(注意,当组件可以为零时,我们可以完全忽略它,使代码更紧凑,这里我保持那些为了清晰的算法).

正如你可以用任何组件进行这些计算,你可以为任何颜色创建这些条件(甚至可以为渐变,你应该尝试!)。

规则中的另外一个陷阱

当我在测试这些颜色条件是如何起作用的时候,我在规则中发现了一个非常,非常奇怪的限制(Tab Atkins评论说颜色组件的这个问题已经在规则中修复了,但是至今还没有浏览器支持)。耶!他还提到另外一种解决方法是我们可以在rgba中使用百分比, 我几乎都忘记了这个功能,哈哈。)。他被称为“类型检查”,我现在很讨厌它。这意味着如果一个属性仅仅接受一个值,如果你在`calc()`里面有任何分割或非整数,即使结果是整数`,解析类型也不是数值,它将是非数值,这意味着这些属性将不会接受这值。当我们有涉及两个以上可能值的计算,我们需要一个非整数修饰符,这将使我们的计算对于使用颜色或其他整数属性(比如z-index)无效

如下:

calc(255 * (1 - var(--bar)) * (var(--bar) - 2) * -0.5)

rgba()中使用这个将是无效的。起初我认为这样的表现是一个bug,特别是知道颜色函数如何实际接受超出可能范围的值(你可以使用rgba(9001, +9001, -9001, 42)得到一个有效的黄颜色),但这样写浏览器似乎浏览器很难处理。

解决方案?

至今为止都没有一个非常完美的解决方案。在我们的例子中,我们知道期望值和有问题的修饰符, 我们可以预先计算它们,然后向上舍入。是的,这意味着结果值可能不完全相同,在某些情况下我们将失去一些精度。但这比什么都没有好,对吧?

对颜色来讲有另外一个可行的解决方案-我们可以使用hsla来代替rgb,因为它不接受整数,而是接受数字和百分比,因此类型解析中不会有冲突。但是对于像z-index那样的属性而言这个方案是不可行的。但是即使使用这种方法,如果你要将rgb转换为hsl,仍然会有一些精度的损失。但是损失的精度应该比以前的解决方案少。

预处理

当条件是二进制时,仍然可以用手写来完成。但是当我们开始使用更复杂的条件时,或者当我们得到颜色时,我们最好有一些工具,使写入更容易。幸运的是,我们有预处理器。

这是我如何设法在Stylus中快速做到这一点(你可以在CodePen with this code上看到代码)

conditional($var, $values...)
  $result = ''

  // If there is only an array passed, use its contents
  if length($values) == 1
    $values = $values[0]

  // Validating the values and check if we need to do anything at all
  $type = null
  $equal = true

  for $value, $i in $values
    if $i > 0 and $value != $values[0]
      $equal = false

    $value_type = typeof($value)
    $type = $type || $value_type
    if !($type == 'unit' or $type == 'rgba')
      error('Conditional function can accept only numbers or colors')

    if $type != $value_type
      error('Conditional function can accept only same type values')

  // If all the values are equal, just return one of them
  if $equal
    return $values[0]

  // Handling numbers
  if $type == 'unit'
    $result = 'calc('
    $i_count = 0
    for $value, $i in $values
      $multiplier = ''
      $modifier = 1
      $j_count = 0
      for $j in 0..(length($values) - 1)
        if $j != $i
          $j_count = $j_count + 1
          // We could use just the general multiplier,
          // but for 0 and 1 we can simplify it a bit.
          if $j == 0
            $modifier = $modifier * $i
            $multiplier = $multiplier + $var
          else if $j == 1
            $modifier = $modifier * ($j - $i)
            $multiplier = $multiplier + '(1 - ' + $var + ')'
          else
            $modifier = $modifier * ($i - $j)
            $multiplier = $multiplier + '(' + $var + ' - ' + $j + ')'

          if $j_count < length($values) - 1
            $multiplier = $multiplier + ' * '

      // If value is zero, just don't add it there lol
      if $value != 0
        if $modifier != 1
          $multiplier = $multiplier + ' * ' + (1 / $modifier)
        $result = $result + ($i_count > 0 ? ' + ' : '') + $value + ' * ' + $multiplier
        $i_count = $i_count + 1

    $result = $result + ')'

  // Handling colors
  if $type == 'rgba'
    $hues = ()
    $saturations = ()
    $lightnesses = ()
    $alphas = ()

    for $value in $values
      push($hues, unit(hue($value), ''))
      push($saturations, saturation($value))
      push($lightnesses, lightness($value))
      push($alphas, alpha($value))

    $result = 'hsla(' + conditional($var, $hues) + ', ' + conditional($var, $saturations) + ', ' + conditional($var, $lightnesses) + ', ' + conditional($var, $alphas) +  ')'

  return unquote($result)

是的,这里有一大堆代码,但是这个混合可以为数字和颜色生成各种条件,不仅仅是两种可能的条件而是更多。

用法十分简单:

border-width: conditional(var(--foo), 10px, 20px)

第一个参数是我们的变量,第二个参数是当变量等于0时应该应用的值,第三个参数是变量等于1时应该应用的值,以此类推。

上面的调用会生成适当的条件:

border-width: calc(10px * (1 - var(--foo)) + 20px * var(--foo));

And here is a more complex example for the color conditionals: 这里还有一个关于颜色条件的更为复杂的例子

color: conditional(var(--bar), red, lime, rebeccapurple, orange)

Would generate something that you surely wouldn't want to write by hand: 这将会生成一些你确定你不会通过手写来完成的代码:

color: hsla(calc(120 * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 270 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 38.82352941176471 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(100% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 100% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 49.99999999999999% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 100% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(50% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 50% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 40% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 50% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), 1);

注意这里并没有检测`可接受的属性,所以这个并不会对z-index起作用,但是它已经将颜色转成了hsla()以便于更好的管理(虽然这甚至可以增强,因此只有当它需要的时候这样的转换才会发生)。另外一个我没在这个混合中实现的是让值可以使用css变量的能力。这对于非整数是可能的,因为那些值将如条件计算中那样插入。或许当我有空的时候,我会修复这个混合让它不仅仅可以接受数字或者颜色还可以接受变量。目前仍然可以使用本文中解释的算法。

回退

当然,如果你计划实际使用这个,你需要有一个方法来设置回退。对于不支持变量的浏览器,它们很容易:只需在条件声明之前声明回退值:

.block {
    padding: 100px; /* fallback */
    padding: calc(
        100px * ((1 - var(--foo)) * (2 - var(--foo)) / 2) +
         20px * (var(--foo) * (2 - var(--foo))) +
          3px * (var(--foo) * (1 - var(--foo)) / -2)
    );
}

但是当涉及到颜色我们有一个问题: 当变量已经得到支持,事实上(这是规则中另外一个很奇怪的地方),任何包含变量的声明都会被认为是有效的。这意味着我们不可能在CSS中为包含变量的属性设置回退值:

background: blue;
background: I 💩 CSS VAR(--I)ABLES;

在规则中这是有效的CSS。背景会得到初始值而不是回退提供的值(即使显而易见该值的其他部分不正确)。

所以,我们需要在这些情况下提供一个后退-对测试除了变量之外的所有支持用@support包裹起来。

在我们的例子中,我们需要在Firefox中为我们的条件颜色进行如下的包装:

.block {
    color: #f00;
}
@supports (color: rgb(0, calc(0), 0)) {
    .block {
        color: rgba(calc(255 * (1 - var(--foo))), calc(255 * var(--foo)), 0, 1);
  }
}

这里我们在颜色函数中测试一个支持的计算,并仅在这种情况下应用条件颜色。

自动创建这样的回退和可能的,但是我不建议您为它们使用预处理器,因为创建这样的东西的复杂性远不止预处理器提供的功能。

使用案例

我真的不喜欢为显而易见的东西提供用例。 所以我会很简短。 我将不仅描述变量的条件,而且描述一般条件,例如'calc()'的结果。

  • CSS变量的条件对于区分块是完美的。这样,你可以有一些编号的主题,然后将它们应用到块(和嵌套的!)只使用一个CSS变量像--block-variant:1。这不是通过除了变量之外的任何其他方法是可能的,当你想要不同的属性在不同的主题中有不同的值。如果没有条件,你需要有许多不同的变量,并应用所有的案例。
  • 排版。如果可以在变量的条件中使用`和> =,那么对于不同的字体大小可以有一些“规则”,所以你可以设置不同的行高,字体权重和其他属性对给定的字体大小。现在这是可能的,但现是在当你需要有一些“停止”那些值,而不只是从em`s派生的值
  • 响应式设计。很好,如果有条件计算,那么它几乎与那些难以捉摸的“元素查询”相同 - 你可以检查vw或父元素的宽度百分比,并决定在不同情况下应用什么。

如果你找到其他使用案例请告诉我!我相信我自己有更多的案例,但我没有那么好的记忆记住我曾经想用CSS做的所有事情。因为它涵盖了一切。

未来

我真的想看到CSS规范中描述的条件,所以我们不会依赖calc hack,并且可以使用适当的条件为非计算值。现在也不可能有除了严格相等的条件,所以没有“当变量超过X”和其他类似的东西。我没有看到任何理由为什么我们不能在CSS中有适当的条件,所以如果你知道一个规范开发人员,提示他们这个问题。我唯一的希望是,他们不会告诉我们“只是使用JS”或找出原因,为什么是不可能的。在这里,现在已经可以使用hack,不能有任何借口。

相关文章