又一个 `:has` 使用场景:实现类似知乎『展开阅读原文』功能

407 阅读3分钟

image.png

前言 🎙️

结果页列表过长我们一般会要求移动端默认用遮罩将内容遮盖,仅展示前 N 条用户最感兴趣的,点击『展开更多』才展示全部内容,因为移动端尺寸有限,PC 端则默认全部展示即可。

如何做到类似功能?本文将基于知乎的『展开阅读原文』来完成我们的需求。

原理 🧠

CSS 如何让 N 条之后的内容隐藏。本文最关键的两个知识点:

  • 如何选择 N 条之后的内容::nth-child(n + N)
  • 结合 :has(:nth-child(n + N)) 来选定含有大于 N 条内容的父元素

HTML 🧱

假设我们的结构如下

<div class="result">
    ...
</div>
<div class="result">
    ...
</div>
<div class="result">
    ...
</div>
<div class="result">
    ...
</div>
<div class="result">
    ...
</div>

第一步先用容器 section 将其包裹起来,然后尾部增加一个 div.references-expander 做为我们的遮罩,将容器设置 position: relative;,因为我们需要将遮罩定位成绝对布局盖在内容上面。遮罩里面就一个按钮:文字加一个向下的 icon,整个遮罩的 HTML 部分从知乎 copy 过来即可。按照需求自定义下文字即可,并且给按钮绑定点击事件 expandReferences

<section class="references-wrapper" style="position: relative;">
    <div class="result">
      ...
    </div>
    <div class="result">
        ...
    </div>
    <div class="result">
        ...
    </div>
    <div class="result">
        ...
    </div>
    <div class="result">
        ...
    </div>
    
    
    <!-- 遮罩 -->
    <div class="references-expander">
        <button type="button" class="btn btn-link btn-sm" onclick="expandReferences(this)">
            Expand to read more
            <span style="display:inline-flex;align-items:center">
                <svg width="24" height="24" viewBox="0 0 24 24" class="Zi Zi--ArrowDown ContentItem-arrowIcon" fill="currentColor"><path fill-rule="evenodd" d="M17.776 10.517a.875.875 0 0 1-.248 1.212l-5.05 3.335a.875.875 0 0 1-.964 0L6.47 11.73a.875.875 0 1 1 .965-1.46l4.56 3.015 4.568-3.016a.875.875 0 0 1 1.212.248Z" clip-rule="evenodd"></path></svg>
            </span>
        </button>
    </div>
</section>

CSS ✨

CSS 部分默认隐藏遮罩,只有移动端才开启 @media (max-width: 767px)。重点来了,移动端也并不是任何情况都开启,必须大于 N 条,我们用 3 条举例。如何用 CSS 实现?

第一步默认隐藏:

.references-expander {
  display: none;
}

接下来要求移动端且大于 3 条,即 ≥ 4才展示。我们用到了 :nth-child(n+4) 来选中 3 条之后的,并且用 :has() 来匹配容器内部有超过 3 条数据,则将遮罩变成显示状态:

@media (max-width: 767px) {
    .references-wrapper:has(.result:nth-child(n + 4)) .references-expander {
        display: flex; /* 满足两个条件则显示遮罩 */
    }
}

那遮罩如何实现,因为我们的遮罩是半透明的。从上到下从透明变成不透明。background-image: linear-gradient(to bottom, transparent, #ffffff 48px); 即可实现,遮罩部分整体样式如下:

  .references-expander {
    display: none;

    width: 100%;
    height: 110px;

    position: absolute;
    bottom: 0;
    background-image: linear-gradient(to bottom, transparent, #ffffff 48px);

    justify-content: center;
    align-items: flex-end;
  }

  @media (max-width: 767px) {
      /* 到 JS 部分会讲到 */
      .references-wrapper.expanded.expanded.expanded.expanded .result {
        display: block;
      }
      .references-wrapper:has(.result:nth-child(n + 4)) .result:nth-child(n + 4) {
        display: none;
      }

      .references-wrapper:has(.result:nth-child(n + 4)) .references-expander {
        display: flex;
      }
  }

JS 🔗

点击展开逻辑,其实只需 CSS 即可实现 radio 结合 :checked 伪类,这里为了理解简单,还是用 JS 监听点击事件结合 CSS 实现。最主要逻辑是点击遮罩时给其容器增加 .expanded,即让下面 CSS 生效,将第三条之后的所有数据展示出来:

/* 重复使用类名达到增加 CSS 权重的目的 */
.references-wrapper.expanded.expanded.expanded.expanded .result {
    display: block;
}

JS 部分很容易理解,主要解释 closest:找到最近的自己或祖先元素。

  /**
   * @param {HTMLButtonElement} btn
   */
  window.expandReferences = (btn) => {
    // 这句也可以直接 btn.closest('.references-expander').remove() 更简洁
    btn.closest('.references-expander').style.display = 'none';
    
    btn.closest('.references-wrapper').classList.toggle('expanded');
  }

效果

注意请在窄屏幕下才可看到展开功能

总结 🎯

本文只使用了少量的必要的 JS 和一些 CSS 高级语法实现了移动端遮罩效果。涉及到的知识点:

  • 媒体查询
  • :has() 重点
  • :nth-child(n + N) 重点
  • linear-gradient
  • closest
  • classList.toggle
  • 最后一条:可以重复使用类名达到增加 CSS 权重的目的