如何使用 WAAPI 动画 Details 元素

Avatar of Louis Hoebregts
Louis Hoebregts on

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费信用额度!

在网站上,为手风琴制作动画一直是最常被问到的动画之一。 有趣的事实:jQuery 的 slideDown() 函数在 2006 年的 第一个版本 中就已经可用。

在本文中,我们将了解如何使用 Web 动画 API 动画原生 <details> 元素。

HTML 设置

首先,让我们看看我们将如何构建此动画所需的标记。

<details> 元素需要一个 <summary> 元素。 摘要是在手风琴关闭时可见的内容。
<details> 中的所有其他元素都是手风琴内部内容的一部分。 为了便于我们动画化该内容,我们将其包装在 <div> 内。

<details>
  <summary>Summary of the accordion</summary>
  <div class="content">
    <p>
      Lorem, ipsum dolor sit amet consectetur adipisicing elit.
      Modi unde, ex rem voluptates autem aliquid veniam quis temporibus repudiandae illo, nostrum, pariatur quae!
      At animi modi dignissimos corrupti placeat voluptatum!
    </p>
  </div>
</details>

Accordion 类

为了使我们的代码更具可重用性,我们应该创建一个 Accordion 类。 通过这样做,我们可以在页面上的每个 <details> 元素上调用 new Accordion()

class Accordion {
  // The default constructor for each accordion
  constructor() {}

  // Function called when user clicks on the summary
  onClick() {}

  // Function called to close the content with an animation
  shrink() {}

  // Function called to open the element after click
  open() {}

  // Function called to expand the content with an animation
  expand() {}

  // Callback when the shrink or expand animations are done
  onAnimationFinish() {}
}

Constructor()

构造函数是我们保存每个手风琴所需的所有数据的地方。

constructor(el) {
  // Store the <details> element
  this.el = el;
  // Store the <summary> element
  this.summary = el.querySelector('summary');
  // Store the <div class="content"> element
  this.content = el.querySelector('.content');

  // Store the animation object (so we can cancel it, if needed)
  this.animation = null;
  // Store if the element is closing
  this.isClosing = false;
  // Store if the element is expanding
  this.isExpanding = false;
  // Detect user clicks on the summary element
  this.summary.addEventListener('click', (e) => this.onClick(e));
}

onClick()

onClick() 函数中,您会注意到我们正在检查元素是否正在动画(关闭或展开)。 我们需要在用户在动画过程中点击手风琴时这样做。 如果快速点击,我们不希望手风琴从完全打开跳到完全关闭。

<details> 元素有一个属性 [open],当我们打开元素时,该属性由浏览器应用到它。 我们可以通过使用 this.el.open 检查元素的 open 属性来获取该属性的值。

onClick(e) {
  // Stop default behaviour from the browser
  e.preventDefault();
  // Add an overflow on the <details> to avoid content overflowing
  this.el.style.overflow = 'hidden';
  // Check if the element is being closed or is already closed
  if (this.isClosing || !this.el.open) {
    this.open();
  // Check if the element is being openned or is already open
  } else if (this.isExpanding || this.el.open) {
    this.shrink();
  }
}

shrink()

此缩小函数使用 WAAPI .animate() 函数。 您可以在 MDN 文档 中详细了解它。 WAAPI 与 CSS @keyframes 非常相似。 我们需要定义动画的开始和结束关键帧。 在这种情况下,我们只需要两个关键帧,第一个是元素的当前高度,第二个是元素关闭时的 <details> 元素的高度。 当前高度存储在 startHeight 变量中。 关闭高度存储在 endHeight 变量中,等于 <summary> 的高度。

shrink() {
  // Set the element as "being closed"
  this.isClosing = true;

  // Store the current height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the height of the summary
  const endHeight = `${this.summary.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow or fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });

  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(false);
  // If the animation is cancelled, isClosing variable is set to false
  this.animation.oncancel = () => this.isClosing = false;
}

open()

当我们想要展开手风琴时,会调用 open 函数。 此函数尚不控制手风琴的动画。 首先,我们计算 <details> 元素的高度,然后使用内联样式将此高度应用于它。 完成后,我们可以设置其上的 open 属性,以使内容可见,但隐藏起来,因为我们在元素上有一个 overflow: hidden 和一个固定高度。 然后,我们等待下一帧调用展开函数并动画化元素。

open() {
  // Apply a fixed height on the element
  this.el.style.height = `${this.el.offsetHeight}px`;
  // Force the [open] attribute on the details element
  this.el.open = true;
  // Wait for the next frame to call the expand function
  window.requestAnimationFrame(() => this.expand());
}

expand()

展开函数类似于 shrink 函数,但它不是从当前高度动画到关闭高度,而是从元素的高度动画到结束高度。 该结束高度等于摘要的高度加上内部内容的高度。

expand() {
  // Set the element as "being expanding"
  this.isExpanding = true;
  // Get the current fixed height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the open height of the element (summary height + content height)
  const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow of fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });
  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(true);
  // If the animation is cancelled, isExpanding variable is set to false
  this.animation.oncancel = () => this.isExpanding = false;
}

onAnimationFinish()

此函数在缩小或展开动画结束时调用。 正如您所见,有一个参数 [open],当手风琴打开时,该参数被设置为 true,允许我们设置元素上的 [open] HTML 属性,因为它不再由浏览器处理。

onAnimationFinish(open) {
  // Set the open attribute based on the parameter
  this.el.open = open;
  // Clear the stored animation
  this.animation = null;
  // Reset isClosing & isExpanding
  this.isClosing = false;
  this.isExpanding = false;
  // Remove the overflow hidden and the fixed height
  this.el.style.height = this.el.style.overflow = '';
}

设置手风琴

Whew,我们完成了代码中最重要的一部分!

剩下的就是为 HTML 中的每个 <details> 元素使用我们的 Accordion 类。 为此,我们在 <details> 标签上使用 querySelectorAll,并为每个标签创建一个新的 Accordion 实例。

document.querySelectorAll('details').forEach((el) => {
  new Accordion(el);
});

笔记

为了计算关闭高度和打开高度,我们需要确保 <summary> 和内容始终具有相同的高度。

例如,不要尝试在摘要打开时添加填充,因为这会导致动画过程中出现跳跃。 内部内容也是如此——它应该具有固定高度,我们应该避免在打开动画过程中可能改变高度的内容。

此外,不要在摘要和内容之间添加边距,因为它不会计算关键帧的高度。 相反,直接在内容上使用填充来添加一些间距。

结束

瞧,我们在没有任何库的情况下,用 JavaScript 做了一个漂亮的手风琴动画!🌈