Vivia Preview

Theme Toggle

关于为了不使用JS而折腾半天的CSS 其一

在这个深入探索CSS奥秘的文章中,我们揭示了如何仅凭CSS(以及一点点JavaScript辅助)实现一个响应式搜索表单布局,该布局具备自动调整列数、动态显示/隐藏操作按钮组,以及利用CSS动画控制展开收起按钮的技巧。

lastModified: 2024-07-19

事情背景

今天一个盆友的公司有一个需求, 一个搜索表单, 具有展开和收起的功能, 要求操作按钮组永远在最右边. 示意图 如上图所示, 当字段数量有一行的时候, 操作按钮组在新的一行的最有边. 当字段数量不满一行的时候, 操作按钮组在未满的一行. 当字段数量超过两行的时候, 则第二行的最后一个显示为操作按钮组, 并且有展开按钮.

展开后的按钮位置同未展开样式一样.

同时要求根据分辨率不同, 控制每行显示的数量, 比如1200px展示4个, 992px展示3个, 768px展示2个, 576px展示1个...

同时展开的按钮也要根据根据当前分辨率和数量决定是否显示, 比如有六个字段时:

  • 1200px时, 展开按钮不显示, 第一行四个, 第二行两个, 加一个操作按钮组

  • 992px时, 展开按钮显示, 第一行三个, 第二行两个, 加一个操作按钮组

  • 768px时, 展开按钮显示, 第一行两个, 第二行一个, 加一个操作按钮组

以此类推.

思路

flex

第一个想到的是flex布局, 代码如下.

html

<div class="container">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
  <div class="cell">7</div>
  <div class="cell buttons">button</div>
</div>

css

.container {
  --cols: 4;
  display: flex;
  flex-wrap: wrap;
}
.container .cell {
  min-width: 0;
  overflow: hidden;
  word-break: break-all;  
  flex: 0 0 calc(100% / var(--cols));
  height: 60px;
  border: solid 1px #000;
  box-sizing: border-box;
}
.container .buttons {
  margin-left: auto;
}

效果如下: flex

使用flex.cell元素均等分父元素, 并且flex-basis为25%, 要求所有元素的缩放和扩张.

然后根据分辨率设置单独的flex-basis, 比如显示三个的时候设置为33.33%, 两个的时候设置为50%.

然后让第N * 2 - 1.cell后的.cell都隐藏.

css

@media (min-width: 768px) and (max-width: 991px) {
  .container {
    --cols: 2;
  }
  .container > .cell:nth-child(3) ~ .cell:not(.buttons) {
    display: none;
  }
}

@media (min-width: 992px) and (max-width: 1199px) {
  .container {
    --cols: 3;
  }
  .container .cell:nth-child(5) ~ .cell:not(.buttons) {
    display: none;
  }
}

@media (min-width: 1200px) {
  .container {
    --cols: 4;
  }
  .container .cell:nth-child(7) ~ .cell:not(.buttons) {
    display: none;
  }
}

结果如下: flex-2 flex-3

正常显示, 要是展开, 只需要将隐藏的样式覆盖即可.

CSS

.container.expanded > .cell {
  /* 这里为了方便使用了 !important */
  display: block !important;
}

html

<div class="container expanded">
  <div class="cell">1</div>
  ....
  <div class="cell buttons">button</div>
</div>

效果如下 flex-expanded

基于flex布局的实现, 基本就完成了.

grid

使用flex是能够实现, 但是这种网格式布局还是有更适合的方式, 就是grid.

CSS

.container {
  --cols: 4;
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
}

.container > .cell {
  min-width: 0;
  overflow: hidden;
  word-break: break-all;
  height: 60px;
  border: solid 1px #000;
}

.container > .cell.buttons {
  grid-column-start: var(--cols);
}

效果如下:

grid

无需给子元素设置宽度, 父元素设置grid-template-columns即可. 同时也不用设置box-sizing来解决border等影响宽度的问题. 按钮使用grid-column-start属性固定在最后一列.

响应式部分和flex一样.

对比

  • flex布局的实现较为麻烦, 需要设置flex-basisbox-sizing来确保宽度,
  • grid布局就相对简单一点, 只需要规定列数, 使用1fr就可以自动均分空间. grid 在较低版本的浏览器中, 无法使用gap来设置间距, 而是使用grid-gap.

展开和收起

如何只通过CSS来控制展开收起按钮的显示与否呢?

思路

由于CSS无法获取子集的数量, 因此这里只能使用JS来设置. 同时CSS也无法进行大于小于之类的逻辑判断, 因此实现起来略有麻烦.

经过一阵思考, 想到了使用animation来实现, 在CSS中唯一能够主动变化的属性? 单独设置displayanimation中是无效的, 所以需要配合一些其他的能够过渡的属性来操作.

CSS

@keyframes show {
  0% {
    display: block;
    opacity: 1;
  }

  100% {
    display: none;
    opacity: 0;
  }
}

然后就是如何执行这个动画, 并且应该怎么执行了.

首先需要的是知道子集的总数量, 用来判断是否该显示按钮, 这里使用vuejs来简化部分实现:

html

<div class="container">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
  <div class="cell">7</div>
  <div class="cell">8</div>
  <div class="cell">9</div>
  <div class="cell buttons">button</div>
</div>
<div class="control">control</div>

javascript

const data = ref(...)
const len = computed(() => data.value.length)

css

.control {
  --n: v-bind(len);
  --cols: 4;
}

使用vuejsv-bind in css的语法糖绑定了len变量, 然后在CSS中就可以使用--n来获取这个变量了.

然后就是如何根据数量和分辨率控制按钮的显示和隐藏了.

animation执行过后会返回初始状态, 所以需要添加一个animation-fill-mode: forwards;来保持动画执行后的状态, 即保留display的状态.

随后需要在控制动画是否执行, 使用animation-play-state是不行的, 因为CSS没有方式能够判断是否应该执行动画, 所以这里使用animation-iteration-count来控制动画的执行.

当需要隐藏的时候, 就让动画的执行次数大于一次, 当需要显示的时候, 就让动画的执行次数为0.

然后通过媒体查询和数量以及前面的列数, 来设置动画的执行次数.

比如当显示4列的时候, 使用显示列数减去总数量来判断是否应该显示展开.

CSS

.control {
  --cols: 4;
  --n: v-bind(len);
  --run-count: min(calc((var(--cols) * 2 ) - var(--n)), 1);
  animation-name: show;
  animation-fill-mode: forwards;
  animation-iteration-count: var(--run-count);
}

因为最多显示两行, 因此将列数*2来计算出显示出了多少个元素.

如果使用显示列数减去总数量后, 小于0则代表总数量大于显示的数量, 需要显示展开按钮, 等于0则代表总数量等于显示的数量, 不需要显示按钮.

同时因为动画执行次数无法为负数, 所以小于0也就不会执行动画了, 因为被视为了无效设置, 默认就会显示按钮, 同时等于0也不会执行.

为什么不是列数*2-1排除掉末尾的按钮呢, 因为显示列数大于总数量的时候是不需要显示按钮的, 因此在相同的时候需要让值为正数, 让动画执行一次.

例如一行4个总数7个的时候, (4*2)-7=1(4*2-1)-7=0 会导致动画没有执行, 不执行就不会隐藏, 但是这里是需要隐藏的.

而数量为8的时候, 因为相差为0, 所以不会执行动画, 所以会显示按钮.再大的总数会成为负数, 导致动画失效.

为了防止当显示数量大于总数量多个的时候, 动画的多次执行, 所以使用min函数限制执行最大次数为1.

CSS

@media screen and (min-width: 481px) and (max-width: 767px) {
  .control {
    --cols: 2;
  }
}

@media screen and (min-width: 769px) {
  .control {
    --cols: 3;
  }
}

@media screen and (min-width: 1200px) {
  .control {
    --cols: 4;
  }
}

由此就完成了基本全由CSS控制的按钮显隐, 只需要JS传递总数量, 其他的行为均为CSS完成.

总结

之后还有不通过JS来控制展开行为之类的, 由于这部分内容就比较简单了, 所以就不在这里多做赘述. 总之这是一次有趣的CSS应用经验, 如果不认真去想的话, 大概第一反应都是使用JS来实现吧.

不过虽然CSS能够实现, 但是也确实相对来说比较麻烦, 需要一些技巧来实现. 同时需要对CSS的能力有一定的认知, 现在的CSS可以说是排除兼容性问题后, 已经非常强大了(虽然和本文无关).

最后附上完整的grid实现代码:

html

<div class="container">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
  <div class="cell">7</div>
  <div class="cell buttons">button</div>
</div>
<div class="control">control</div>

javascript

const data = ref(...)
const len = computed(() => data.value.length)

css

.container {
  --cols: 4;
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
}

.container > .cell {
  min-width: 0;
  overflow: hidden;
  word-break: break-all;
  height: 60px;
  border: solid 1px #000;
}

.container > .cell.buttons {
  grid-column-start: var(--cols);
}

.control {
  --cols: 4;
  --n: v-bind(len);
  --run-count: min(calc((var(--cols) * 2) - var(--n)), 1);
  animation-name: show;
  animation-fill-mode: forwards;
  animation-iteration-count: var(--run-count);
}

@media (min-width: 768px) and (max-width: 991px) {
  .container {
    --cols: 2;
  }
  .control {
    --cols: 2;
  }
  .container > .cell:nth-child(3) ~ .cell:not(.buttons) {
    display: none;
  }
}

@media (min-width: 992px) and (max-width: 1199px) {
  .control {
    --cols: 3;
  }
  .container {
    --cols: 3;
  }
  .container .cell:nth-child(5) ~ .cell:not(.buttons) {
    display: none;
  }
}

@media (min-width: 1200px) {
  .container {
    --cols: 4;
  }
  .control {
    --cols: 4;
  }
  .container .cell:nth-child(7) ~ .cell:not(.buttons) {
    display: none;
  }
}

@keyframes show {
  0% {
    display: block;
    opacity: 1;
  }

  100% {
    display: none;
    opacity: 0;
  }
}

AI结语

本文不仅是一次对CSS极限能力的探索,也是对传统布局与交互实现方式的一次挑战。虽然过程中不得不轻微触碰JavaScript来辅助,但核心逻辑与控制依旧牢牢掌握在CSS手中,证明了在某些场景下,CSS也能成为实现复杂界面逻辑的得力工具。对于前端开发者而言,这无疑是一次启发思维、拓宽视野的宝贵经验分享。

© 9999 Vivia Name

Powered by Nextjs & Theme Vivia

主题完全模仿 Vivia 主题, 有些许差异, 及使用nextjs乱写

Theme Toggle