Обработка событий CSS-переходов в JavaScript

Обработка событий CSS-переходов в JavaScript
Содержание:
  1. CSS-переходы и их события
  2. Пример 1. Плавное изменение цвета фона элемента
  3. Пример 2. Плавное перемещение элемента
  4. Пример 3. Слайдер с плавными переходами
  5. Комментарии

В этой статье мы изучим какие события возникают в DOM при CSS-переходах и как их можно использовать в JavaScript. Рассмотрим несколько примеров на эту тему.

CSS-переходы и их события

Как вы уже знаете для создания простой анимации в CSS можно использовать переходы. Суть здесь заключается в том, что с помощью них мы можем указать браузеру то, что значение какого-то CSS-свойства нужно изменить не сразу, а плавно в течении некоторого количества времени. После этого, для того чтобы переход начался, нам нужно будет всего лишь изменить значение этого CSS-свойства, и браузер сам выполнит эту анимацию.

Очень часто на сайтах и в веб-приложениях запуск CSS-перехода выполняется при наступлении каких-то определенных событий. Например, при клике на элемент или при наведении на него курсора.

Для задания CSS-перехода используются следующие свойства:

  • transition-property – для указания CSS-свойства, которое нужно анимировать;
  • transition-duration – длительность перехода;
  • transition-timing-function – задает как должна изменяться скорость во время CSS-перехода;
  • transition-delay – время задержки перед началом перехода.

Например, мы хотим, чтобы изменения цвета фона (background-color) происходило плавно в течение 1 секунды. При этом нам нужно, чтобы этот процесс начинался не сразу, а через 0,1 секунды после создания CSS-перехода. Кроме этого, необходимо чтобы переход начинался и заканчивался медленно, или другими словами, чтобы анимация выполнялась посредством функции ease-in-out:

HTML
<style>
  .box {
    background-color: #fffde7;
    transition: background-color;
    transition-duration: 1s;
    transition-timing-function: ease-in-out;
    transition-delay: 0.1s;
  }
  .box:hover {
    background-color: #ffeb3b;
  }
</style>
<div class="box">Lorem</div>

В CSS также можно записать все эти свойства кратко, используя transition:

CSS
.box {
  background-color: #fffde7;
  transition: background-color 1s ease-in-out 0.1s;
}

При CSS-переходах могут возникать следующие события в DOM:

  • transitionrun – при создании CSS-перехода, то есть до того, как он реально начался;
  • transitionstart – когда CSS-переход фактически начался;
  • transitionend – при завершении выполнения перехода;
  • transitioncancel – при отмене CSS-перехода.

Событие transitionrun возникает при создании CSS-перехода, то есть перед transition-delay. В то время transitionstart возникает, когда CSS-переход уже начался, то есть сразу после окончания transition-delay.

Событие transitionend возникает после завершения CSS-перехода. Если CSS-переход удаляется до завершения, то событие transitionend сгенерировано не будет. Например, если мы во время перехода удалили свойство transition или установили для элемента свойство display со значением none.

При удалении анимации до её завершения генерируется другое событие, а именно transitioncancel.

Добавление обработчиков к этим событиям осуществляется как обычно:

JavaScript
el.addEventListener('transitionend', () => {
  // действия, которые нужно выполнить при возникновении события transitionend на элементе el
});

Здесь мы добавили обработчик события transitionend с помощью метода addEventListener.

Пример 1. Плавное изменение цвета фона элемента

В этом примере создадим HTML-элемент .box. При нажатии на который мы будем с помощью JavaScript менять цвет его фона на случайный. Но изменение цвета будем производить не мгновенно, а плавно с помощью CSS-перехода:

Обработка событий, возникающих при CSS-переходах в JavaScript

Демо

HTML-код элемента:

HTML
<div class="box">#f2c2b4</div>

Стили, с помощью которых мы зададим для .box определенные размеры, цвет фона, CSS-переход, а также некоторые другие вещи:

CSS
.box {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100px;
  height: 100px;
  margin: 20px;
  color: black;
  font-size: 18px;
  font-family: monospace;
  background-color: #f2c2b4;
  cursor: pointer;
  transition: background-color 0.5s ease-in-out 0.1s;
}

Код JavaScript, написанный с использованием ES6 классов:

JavaScript
class ColorBox {
  #el;
  #isTransition = false;
  constructor(selector) {
    this.#el = document.querySelector(selector);
    this.#addEventListeners();
  }
  #addEventListeners() {
    this.#el.addEventListener('click', (e) => {
      if (this.#isTransition) {
        console.log('CSS-переход ещë не закончился', new Date().toLocaleTimeString());
        return;
      }
      this.#isTransition = true;
      const hex = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
      e.target.style.backgroundColor = hex;
      const [r, g, b] = e.target.style.backgroundColor.replace(/[^\d,]/g, '').split(',');
      const brightness = Math.floor((r * 299 + g * 587 + b * 114) / 1000);
      e.target.style.color = brightness < 128 ? 'white' : 'black';
      e.target.textContent = hex;
    });
    this.#el.addEventListener('transitionrun', () => {
      console.log('CSS-переход создан', new Date().toLocaleTimeString());
    });
    this.#el.addEventListener('transitionstart', () => {
      console.log('CSS-переход начался', new Date().toLocaleTimeString());
    });
    this.#el.addEventListener('transitionend', () => {
      this.#isTransition = false;
      console.log('CSS-переход закончился', new Date().toLocaleTimeString());
    });
  }
}

const colorBox = new ColorBox('.box');

В коде мы обрабатываем следующие события, возникающие на элементе .box:

  • click – при клике;
  • transitionrun – при создании CSS-перехода;
  • transitionstart – при начале CSS-перехода;
  • transitionend – при окончании CSS-перехода.

При клике мы проверяем, выполняется ли CSS-переход, то есть истинно ли значение свойства this.#isTransition. Если так, то выводим сообщение в консоль и прекращаем дальнейшее выполнение функции. В противном случае мы устанавливаем приватному свойствуthis.#isTransition значение true.

Далее мы генерируем случайный цвет и сохраняем его в переменную hex. После этого присваиваем CSS-свойству background-color значение hex. Затем мы объявляем переменные r, g и b и сохраняем в них компоненты соответственно красного, зеленого и синего цветов. Они нам понадобятся для определения яркости цвета и установки в зависимости от этого для текста черного или белого цвета. В конце с помощью textContent мы выведем текст цвета в формате HEX в качестве содержимого .box.

В обработчиках событий transitionrun, transitionstart и transitionend мы будем просто выводить соответствующее сообщение в консоль. Кроме этого в transitionend будем ещё дополнительно устанавливать приватному свойству this.#isTransition значение false, которое будет говорить о том, что CSS-переход завершился. И можно создавать новый при клике на .box.

Пример 2. Плавное перемещение элемента

В этом примере напишем код для плавного перемещения элемента с использованием translateX и transition:

Пример обработки событий CSS-переходов transitionrun, transitionstart, transitionend и transitioncancel в JavaScript

Демо

Для этого нам понадобится вот такой HTML-код:

HTML
<div class="box-wrapper container">
  <div class="box"></div>
</div>
<div class="controls container">
  <button type="button" class="start">start</button>
  <button type="button" class="stop" disabled>stop</button>
</div>
<div class="logs container"></div>

Здесь .box-wrapper – это контейнер внутри которого мы будем плавно двигать элемент .box с помощью transform: translateX(300px) в течение 3 секунд. Запускать это действие будем при клике по кнопке «start», а в JavaScript в обработчике этого события будем добавлять к .box класс translating. Прервать анимацию, можно будет при клике по кнопке «stop».

В элемент .logs мы будем помещать события, связанные с CSS-переходом в порядке их возникновения.

Для оформления, а также для установки transform и transition напишем следующий CSS-код:

CSS
.container {
  width: 400px;
  margin-right: auto;
  margin-bottom: 1rem;
  margin-left: auto;
}
.box-wrapper {
  background-color: #dcedc8;
}
.box {
  width: 100px;
  height: 100px;
  background-color: #8bc34a;
}
.translating {
  transform: translateX(300px);
  transition: transform 3s ease-in-out 0.5s;
}
.controls {
  margin-bottom: 1rem;
  text-align: center;
}
.logs {
  min-height: 100px;
  padding: 20px 15px;
  font-size: 14px;
  font-family: monospace;
  background-color: #fff9c4;
}

JavaScript-код:

JavaScript
const elBox = document.querySelector('.box');
const elLogs = document.querySelector('.logs');
const btnStart = document.querySelector('.start');
const btnStop = document.querySelector('.stop');
btnStart.onclick = () => elBox.classList.add('translating');
btnStop.onclick = () => elBox.classList.remove('translating');
elBox.addEventListener('transitionrun', (e) => {
  btnStart.disabled = true;
  const now = new Date().toLocaleTimeString();
  const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${now}</div>`;
  elLogs.innerHTML = html;
});
elBox.addEventListener('transitionstart', (e) => {
  btnStop.disabled = false;
  const now = new Date().toLocaleTimeString();
  const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${now}</div>`;
  elLogs.insertAdjacentHTML('beforeend', html);
});
elBox.addEventListener('transitionend', (e) => {
  btnStart.disabled = false;
  btnStop.disabled = true;
  const now = new Date().toLocaleTimeString();
  const elapsedTime = Math.round(e.elapsedTime * 100) / 100;
  const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${elapsedTime}s ${now}</div>`;
  elLogs.insertAdjacentHTML('beforeend', html);
});
elBox.addEventListener('transitioncancel', (e) => {
  btnStart.disabled = false; btnStop.disabled = true;
  const now = new Date().toLocaleTimeString();
  const elapsedTime = Math.round(e.elapsedTime * 100) / 100;
  const html = `<div>> <b>${e.type}</b> ${e.propertyName} ${elapsedTime}s ${now}</div>`;
  elLogs.insertAdjacentHTML('beforeend', html);
});

Что делает этот код? При клике по кнопке .start мы будем добавлять к элементу .box класс translating и тем самым создавать CSS-переход. При клике по .stop убираем у элемента .box класс translating и тем самым удаляем CSS-переход.

Остальные обработчики событий просто выводят сообщения в элемент .logs со следующей информацией:

  • type – тип события;
  • propertyName – имя CSS-свойства для перехода;
  • elapsedTime – количество времени в секундах, в течение которого выполнялся переход.

Пример 3. Слайдер с плавными переходами

В этом примере мы создадим простой слайдер с зацикливанием или иными словами с бесконечной прокруткой.

Пример создания простого слайдера на JavaScript с использованием CSS-переходов, transition, и событий transitionrun и transitionend

Демо

Начнём с написания HTML-кода:

HTML
<div class="slider-wrapper">
  <div class="slider">
    <div class="slider-item slider-item-active">1</div>
    <div class="slider-item">2</div>
    <div class="slider-item">3</div>
    <div class="slider-item">4</div>
  </div>
</div>
<div class="slider-controls">
  <button type="button" class="slider-prev">prev</button>
  <button type="button" class="slider-next">next</button>
</div>

Здесь .slider-item – это слайд. В этом слайдере их 4. Для смены слайдов будем использовать кнопки .slider-prev и .slider-next.

Для размещения элементов в слайдере будем использовать технологию CSS Flexbox. В качестве ширины установим .slider-item значение 200px:

CSS
.slider-wrapper {
  width: 200px;
  overflow-x: hidden;
}
.slider {
  display: flex;
}
.slider-item {
  flex: 0 0 200px;
  height: 150px;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
}

Смещать плавно слайды будем с помощью CSS-функции translateX и transition. Здесь мы будем их устанавливать посредством JavaScript:

JavaScript
class Slider {
  constructor(selector) {
    this.slider = document.querySelector(selector);
    this.elFirst = this.slider.firstElementChild;
    this.elLast = this.slider.lastElementChild;
    this.btnPrev = document.querySelector('.slider-prev');
    this.btnNext = document.querySelector('.slider-next');
    this.direction = 'next';
    this.width = this.slider.parentElement.getBoundingClientRect().width;
    this.count = this.slider.querySelectorAll('.slider-item').length;
    this.step = this.width * this.count;
    this.elLast.style.transform = `translateX(${-this.step}px)`;
    this.orders = [...this.slider.children].map((value, index) => index);
    this.orders = [...this.orders.slice(-1), ...this.orders.slice(0, -1)];
    this.addEventListener();
  }
  move() {
    let value = new WebKitCSSMatrix(this.slider.style.transform).m41;
    value += this.direction === 'next' ? -this.width : this.width;
    this.slider.style.transform = `translateX(${value}px)`;
    this.slider.style.transition = 'transform 1s ease';
    const el = this.slider.querySelector('.slider-item-active');
    let elNew = this.direction === 'next' ? el.nextElementSibling : el.previousElementSibling;
    if (!elNew) {
      elNew = this.direction === 'next' ? this.elFirst : this.elLast;
    }
    el.classList.remove('slider-item-active');
    elNew.classList.add('slider-item-active');
  }
  addEventListener() {
    this.btnPrev.onclick = () => {
      this.direction = 'prev';
      this.move();
    }
    this.btnNext.onclick = () => {
      this.direction = 'next';
      this.move();
    }
    this.slider.ontransitionrun = () => {
      this.btnPrev.disabled = true;
      this.btnNext.disabled = true;
      const index = this.direction === 'next' ? this.orders[0] : this.orders[this.orders.length - 1];
      const el = this.slider.querySelector(`.slider-item:nth-child(${index + 1})`);
      let value = new WebKitCSSMatrix(el.style.transform).m41;
      value += this.direction === 'next' ? this.step : -this.step;
      el.style.transform = `translateX(${value}px)`;
      if (this.direction === 'prev') {
        this.orders = [...this.orders.slice(-1), ...this.orders.slice(0, -1)];
      } else {
        this.orders = [...this.orders.slice(1), ...this.orders.slice(0, 1)];
      }
    }
    this.slider.ontransitionend = () => {
      this.btnPrev.disabled = false;
      this.btnNext.disabled = false;
    }
  }
}

const slider = new Slider('.slider');

Из интересного здесь то, что порядок следования элементов мы пишем в массив this.orders: [3, 0, 1, 2]. Который затем используем для того, чтобы организовать бесконечную прокрутку слайдера. При движении вправо, нам нужно крайний элемент слева переместить после последнего. А при движении влево, наоборот, крайний элемент справа переместить перед первым.

С помощью this.orders получить такие элементы очень легко:

  • this.orders[0] – крайний элемент слева;
  • this.orders[this.orders.length - 1] – крайний элемент справа.

Перемещение крайнего элемента для зацикливания мы делаем при создании CSS-перехода, то есть в обработчике события ontransitionrun. Здесь мы также переводим кнопки в неактивное состояние:

JavaScript
this.btnPrev.disabled = true;
this.btnNext.disabled = true;

После завершения CSS-перехода делаем их опять активными:

JavaScript
this.slider.ontransitionend = () => {
  this.btnPrev.disabled = false;
  this.btnNext.disabled = false;
}

Комментарии: 0