Доброго времени суток, друзья!
В данной статье я покажу, как можно анимировать нативный элемент «details» с помощью Web Animations API.
Начнем с разметки.
В элемент «details» должен быть вложен элемент «summary». summary — это видимая часть контента при закрытом «аккордеоне».
Любые другие элементы являются частью внутреннего содержимого аккордеона. Для облегчения нашей задаче обернем этот контент в div с классом «content».
<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 на странице.
class Accordion {
constructor() {}
// функция, вызываемая при нажатии на summary
onClick() {}
// функция, вызываемая для анимированного скрытия контента
shrink() {}
// функция, вызываемая для раскрытия элемента после клика
open() {}
// функция, вызываемая для анимированного показа контента
expand() {}
// коллбэк, вызываемый после завершения shrink или expand
onAnimationFinish() {}
}
constructor()
Конструктор служит для хранения необходимых аккордеону данных.
constructor(el) {
// сохраняем details
this.el = el
// сохраняем summary
this.summary = el.querySelector('summary')
// сохраняем div с классом "content"
this.content = el.querySelector('.content')
// сохраняем объект анимации (для ее отмены при необходимости)
this.animation = null
// находится ли элемент в процессе закрытия?
this.isClosing = false
// находится ли элемент в процессе раскрытия?
this.isExpanding = false
// определяем клик по summary
this.summary.addEventListener('click', (e) => this.onClick(e))
}
onClick()
В функции «onClick» мы проверяем, находится ли элемент в процессе анимирования (закрытия или раскрытия). Нам необходимо это сделать для случая, когда пользователь кликает по аккордеону до завершения анимации. Мы ведь не хотим, чтобы аккордеон «прыгал» от полностью открытого до полностью закрытого.
Элемент «details» имеет атрибут «open», добавляемый браузером при открытии элемента. Мы можем получить значение этого атрибута посредством this.el.open.
onClick(e) {
// отменяем стандартное поведение браузера
e.preventDefault()
// добавляем к details свойство "overflow" со значением "hidden" во избежание переполнения контента
this.el.style.overflow = 'hidden'
// проверяем, находится ли элемент в процессе закрытия или уже закрыт
if (this.isClosing || !this.el.open) {
this.open()
// проверяем, находится ли элемент в процессе открытия или уже открыт
} else if (this.isExpanding || this.el.open) {
this.shrink()
}
}
shrink()
Функция «shrink» использует функцию «animate» WAAPI. Вы можете почитать об этой функции здесь. WAAPI очень похож на инструкцию «keyframes» CSS в том, что мы должны определить ключевые кадры анимации. В данном случае нам требуется лишь два таких кадра: первый — текущая высота элемента «details» (в открытом состоянии), второй — высота закрытого details (высота summary).
shrink() {
// фиксируем начало закрытия элемента
this.isClosing = true
// сохраняем текущую высоту элемента
const startHeight = `${this.el.offsetHeight}px`
// рассчитываем высоту summary
const endHeight = `${this.summary.offsetHeight}px`
// если анимация уже запущена
if (this.animation) {
// отменяем ее
this.animation.cancel()
}
// запускаем WAAPI анимацию
this.animation = this.el.animate({
// устанавливаем ключевые кадры
height: [startHeight, endHeight]
}, {
// если анимация кажется вам слишком быстрой или слишком медленной, то вы можете изменить значение данного свойства (duration - продолжительность)
duration: 400,
// вы также можете изменить значение этого свойства (easing (animation-timing-function) - временная функция)
easing: 'ease-out'
})
// после завершения анимации вызываем onAnimationFinish()
this.animation.onfinish = () => this.onAnimationFinish(false)
// если анимация отменена, присваиваем переменной "isClosing" значение "false"
this.animation.oncancel = () => this.isClosing = false
}
open()
Функция «open» вызывается, когда мы хотим раскрыть аккордеон. Данная функция не управляет анимацией аккордеона. Сначала мы рассчитываем высоту элемента «details» и добавляем ему соответствующие встроенные стили. После того, как это сделано, мы можем добавить ему атрибут «open» для того, чтобы сделать контент видимым, но в тоже время скрытым благодаря overflow: hidden и фиксированной высоте элемента. Далее мы ожидаем следующего кадра для вызова функции «expand» и анимирования элемента.
open() {
// устанавливаем элементу фиксированную высоту
this.el.style.height = `${this.el.offsetHeight}px`
// добавляем details атрибут "open"
this.el.open = true
// ожидаем следующего кадра для вызова функции "expand"
requestAnimationFrame(() => this.expand())
}
expand()
Функция «expand» похожа на функцию «shrink», но вместо анимирования от текущей высоты элемента до его высоты в закрытом состоянии, мы анимируем от высоты элемента до полной высоты. Полная высота равняется высоте summary плюс высота внутреннего содержимого.
expand() {
// фиксируем начало раскрытия элемента
this.isExpanding = true
// получаем фиксированную высоту элемента
const startHeight = `${this.el.offsetHeight}px`
// рассчитываем высоту открытого элемента (высота summary + высота содержимого)
const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`
// если анимация уже запущена
if (this.animation) {
// отменяем ее
this.animation.cancel()
}
// запускаем WAAPI анимацию
this.animation = this.el.animate({
height: [startHeight, endHeight]
}, {
duration: 400,
easing: 'ease-out'
})
this.animation.onfinish = () => this.onAnimationFinish(true)
this.animation.oncancel = () => this.isClosing = false
}
onAnimationFinish()
Данная функция вызывается в конце анимации раскрытия и закрытия details. Она принимает один параметр — логическое значение для атрибута «open», который больше не обрабатывается браузером (если помните, мы отменили стандартное поведение браузера в функции «onClick»).
onAnimationFinish(open) {
// устанавливаем значение атрибута "open"
this.el.open = open
// удаляем переменную, хранящую анимацию
this.animation = null
// сбрасываем значения
this.isClosing = false
this.isExpanding = false
// удаляем overflow и фиксированную высоту
this.el.style.height = this.el.style.overflow = ''
}
Инициализация аккордеонов
Фух! Мы почти закончили.
Все, что осталось сделать, это создать экземпляр класса «Accordion» для каждого элемента «details» на странице.
document.querySelectorAll('details').forEach(el => {
new Accordion(el)
})
Замечания
Для правильного рассчета высоты элемента в открытом и закрытом состояниях summary и контент должны иметь одинаковую высоту на всем протяжении анимации.
Не добавляйте внутренние отсутпы для открытого summary, поскольку это может привести к резким скачкам. Тоже самое справедливо для внутреннего контента — он должен иметь фиксированную высоту, следует избегать изменения его высоты в процессе открытия details.
Также не добавляйте внешние отсутпы между summary и контентом, поскольку они не будут учтены при рассчете высоты в ключевых кадрах. Вместо этого используйте внутренние отступы контента для добавления некоторого пространства.
Заключение
Вот так, легко и просто мы умудрились создать аккордеон на чистом JavaScript.
Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
ivan386
Зачем использовать Javascript там где может справиться CSS?
Max_JK
вот только это решение довольно грязное, если контент больше 2000px то он обрежется, если меньше, то будет задержка при сворачивании
(например если высота блока = 50px то нужно ждать пока transition пробежит 1950px, и только тогда появится видимая анимация, и при том длится она будет только 0.05 секунд вместо заданных 2 секунд),
пока что css не может предложить идеального решения
ivan386
Ответ автора:
Соответственно элемент будет развёрнут до полного своего размера.
Можно перераспределить скорость выполнения анимации по времени так чтобы большая часть времени анимации приходилась на её завершение. Используется для этого animation-time-function.