Створюємо горизонтальний таймлайн з допомогою CSS і JavaScript

345

Від автора: в попередньому пості я розповів вам, як створити адаптивний вертикальний таймлайн з нуля. Сьогодні я покажу вам процес створення горизонтального таймлайна. Як звичайно, щоб зрозуміти, що ми будемо створювати, погляньте на демо нижче (див. велику версію).

У нас багато роботи, так почнемо! Розмітка ідентична розмітці з вертикального таймлайна, тільки є три відмінності:

замість ненумерованного списку ми використовуємо нумерований, так як це семантично правильніше;

є додатковий порожній елемент списку (останній), про який ми поговоримо нижче;

є додатковий елемент.arrows), що відповідає за навігацію по таймлайну.

  • 1934
    Some content here
  • Створюємо горизонтальний таймлайн з допомогою CSS і JavaScript
    Створюємо горизонтальний таймлайн з допомогою CSS і JavaScript

    Спочатку таймлайн виглядає наступним чином:

    Додаємо первинні стилі

    Для спрощення я пропущу стилі шрифтів, кольору і т. д. і перейду до структурних CSS-правил:

    .timeline {
    white-space: nowrap;
    overflow-x: hidden;
    }
    .timeline ol {
    font-size: 0;
    width: 100vw;
    padding: 250px 0;
    transition: all 1s;
    }
    .timeline ol li {
    position: relative;
    display: inline-block;
    list-style-type: none;
    width: 160px;
    height: 3px;
    background: #fff;
    }
    .timeline ol li:last-child {
    width: 280px;
    }
    .timeline ol li:not(:first-child) {
    margin-left: 14px;
    }
    .timeline ol li:not(:last-child)::after {
    content: “;
    position: absolute;
    top: 50%;
    left: calc(100% + 1px);
    bottom: 0;
    width: 12px;
    height: 12px;
    transform: translateY(-50%);
    border-radius: 50%;
    background: #F45B69;
    }

    Ви помітите дві найважливіші речі:

    У списку задані великі верхній і нижній padding’в. Навіщо це потрібно, ми пояснимо в наступній секції.

    У демо нижче ви помітите, що поки що нам не видно всі елементи списку, так як у списку є властивість width: 100vw, а у його батька overflow-x: hidden. Остання властивість «маскує» елементи списку. Трохи пізніше ми зможемо переміщатися по елементах списку за допомогою навігації.

    На даний момент таймлайн виглядає наступним чином (без вмісту):

    Стилі елементів таймлайна

    Тепер необхідно стилізувати теги div (ми будемо називати їх елементи таймлайна), які входять до елементів списку разом з псевдоэлементами ::before.

    Щоб розрізняти стилі для парних і непарних елементів таймлайна, ми будемо використовувати псевдокласи :nth-child(odd): nth-child(even).

    Загальні стилі елементів таймлайна:

    .timeline ol li div {
    position: absolute;
    left: calc(100% + 7px);
    width: 280px;
    padding: 15px;
    font-size: 1rem;
    white-space: normal;
    color: black;
    background: white;
    }
    .timeline ol li div::before {
    content: “;
    position: absolute;
    top: 100%;
    left: 0;
    width: 0;
    height: 0;
    border-style: solid;
    }

    Стилі для непарних елементів:

    .timeline ol li:nth-child(odd) div {
    top: -16px;
    transform: translateY(-100%);
    }
    .timeline ol li:nth-child(odd) div::before {
    top: 100%;
    border-width: 8px 8px 0 0;
    border-color: white transparent transparent transparent;
    }

    І для парних елементів:

    .timeline ol li:nth-child(even) div {
    top: calc(100% + 16px);
    }
    .timeline ol li:nth-child(even) div::before {
    top: -8px;
    border-width: 8px 0 0 8px;
    border-color: transparent transparent transparent white;
    }

    Тепер таймлайн з контентом виглядає так:

    Можливо, ви помітили, що елементи таймлайна абсолютно позиціоновані. Це означає, що вони видалені з нормального потоку в документі. Нам знадобилося задати великі значення верхнього і нижнього padding’а для списку, щоб бути впевненими, що видно весь таймлайн. Якщо прибрати padding, таймлайн буде обрізатися:

    Створюємо горизонтальний таймлайн з допомогою CSS і JavaScript

    Стилі навігації таймлайна

    Тепер потрібно стилізувати кнопки навігації. Не забувайте, що за замовчуванням ми відключаємо кнопку назад і присвоюємо їй клас disabled.

    .timeline .arrows {
    display: flex;
    justify-content: center;
    margin-bottom: 20px;
    }
    .timeline .arrows .arrow__prev {
    margin-right: 20px;
    }
    .timeline .disabled {
    opacity: .5;
    }
    .timeline .arrows img {
    ширина: 45px;
    height: 45px;
    }

    І наш таймлайн став:

    Додаємо інтерактивність

    Базова структура таймлайна готова. Давайте зробимо її інтерактивної!

    Змінні

    Спершу ми поставимо ряд змінних, які нам потім знадобляться.

    const timeline = document.querySelector(“.timeline ol”),
    elH = document.querySelectorAll(“.timeline li > div”),
    arrows = document.querySelectorAll(“.timeline .arrows .arrow”),
    arrowPrev = document.querySelector(“.timeline .arrows .arrow__prev”),
    arrowNext = document.querySelector(“.timeline .arrows .arrow__next”),
    firstItem = document.querySelector(“.timeline li:first-child”),
    lastItem = document.querySelector(“.timeline li:last-child”),
    xScrolling = 280,
    disabledClass = “disabled”;

    Ініціалізація об’єктів

    Коли всі елементи на сторінці готові, викликається функція init.

    window.addEventListener. (“load”, init);

    Ця функція викликає 4 інші функції:

    function init() {
    setEqualHeights(elH);
    animateTl(xScrolling, arrows, timeline);
    setSwipeFn(timeline, arrowPrev, arrowNext);
    setKeyboardFn(arrowPrev, arrowNext);
    }

    Трохи пізніше ми побачимо, що кожна функція виконує певне завдання.

    Елементи таймлайна з однаковою висотою

    Якщо повернутися до попереднього демо, то можна помітити, що у елементів таймлайна різна висота. Це ніяк не впливає не головний функціонал таймлайна, але, можливо, ви захочете, щоб всі елементи мали однакову висоту. Фіксовану висоту можна задати через CSS (простий спосіб), або ж можна встановлювати динамічну висоту, рівну висоті найвищого елементу через JS.

    Другий варіант більш гнучкий і стабільний. Функція, що виконує описані вище дії:

    function setEqualHeights(el) {
    let counter = 0;
    for (let i = 0; i < el.length; i++) {
    const singleHeight = el.offsetHeight;
    if (counter < singleHeight) {
    counter = singleHeight;
    }
    }
    for (let i = 0; i < el.length; i++) {
    el.style.height = `${counter}px`;
    }
    }

    Функція витягає висоту найвищого елементу в таймлайні і задає її за промовчанням для всіх елементів.

    Анимируем таймлайн

    Переходимо до анімації таймлайна. Ми створили функцію, яка буде покроково анімувати таймлайн.

    Першим ділом реєструється обробника події клацання по кнопках:

    function animateTl(scrolling, el, tl) {
    for (let i = 0; i < el.length; i++) {
    el.addEventListener. (“click”, function() {
    // код
    });
    }
    }

    Кожен раз при натисканні ми перевіряємо стан кнопок навігації на disabled, і якщо вони активні, ми відключаємо їх. Так ми точно будемо знати, що до кінця анімації кнопки більше не будуть натискатися.

    Обробник кліка складається з цих рядків:

    if (!arrowPrev.disabled) {
    arrowPrev.disabled = true;
    }
    if (!arrowNext.disabled) {
    arrowNext.disabled = true;
    }

    Далі ми виконуємо наступні кроки:

    Ми перевіряємо, чи був це перший клік по кнопці. Не забувайте, що кнопка назад відключена за замовчуванням, тобто спочатку можна тільки клікнути по кнопці вперед.

    Якщо кнопка натиснута перший раз, ми рухаємо таймлайн на 280px вправо за допомогою властивості transform. Зсув задається змінною xScrolling.

    Якщо ж на кнопку вже натискали, ми отримуємо поточне значення transform і додаємо до нього або видаляємо з нього зсув (280px). Тобто якщо клікнути на кнопку назад, значення transform зменшиться, і таймлайн зрушиться зліва направо. Якщо клікнути на кнопку вперед, значення transform збільшиться, і таймлайн зрушиться праворуч ліворуч.

    Код для цих кроків:

    let counter = 0;
    for (let i = 0; i < el.length; i++) {
    el.addEventListener. (“click”, function() {
    // попередній код
    const sign = (this.classList.contains(“arrow__prev”)) ? “” : “-“;
    if (counter === 0) {
    tl.style.transform = `translateX(-${scrolling}px)`;
    } else {
    const tlStyle = getComputedStyle(tl);
    // якщо потрібно, додайте ще префіксів браузерів
    const tlTransform = tlStyle.getPropertyValue(“-webkit-transform”) || tlStyle.getPropertyValue(“transform”);
    const values = parseInt(tlTransform.split(“,”)[4]) + parseInt(`${sign}${scrolling}`);
    tl.style.transform = `translateX(${values}px)`;
    }
    counter++;
    });
    }

    Чудово! Ми визначили анімацію таймлайна. Тепер необхідно зрозуміти, коли анімація повинна зупинятися. Наш підхід:

    Коли перший елемент таймлайна повністю стає видимим, це означає, що ми досягли початку таймлайна, тому ми відключаємо кнопку назад. Також перевіряємо, щоб кнопка вперед була активною.

    Коли останній елемент таймлайна повністю стає видимим, це означає, що ми досягли кінця таймлайна, тому ми відключаємо кнопку вперед. Також перевіряємо, щоб кнопка назад була активною.

    Не забувайте, що останній елемент порожній, і його ширина дорівнює ширині елементів таймлайна (тобто 280px). Ми поставили це значення (або більше), щоб переконатися, що останній елемент потрапив у поле зору перед відключенням кнопки вперед.

    Щоб визначити, чи потрапив цільової елемент повністю під вьюпорт чи ні, ми візьмемо код для вертикального таймлайна. Код взято з гілки Stack Overflow:

    function isElementInViewport(el) {
    const rect = el.getBoundingClientRect();
    return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
    }

    Поза функції зверху ми задаємо ще один хелпер:

    function setBtnState(el, flag = true) {
    if (flag) {
    el.classList.add(disabledClass);
    } else {
    if (el.classList.contains(disabledClass)) {
    el.classList.remove(disabledClass);
    }
    el.disabled = false;
    }
    }

    Ця функція додає і видаляє клас disabled на елемент на основі значення параметра flag. Також функція вміє змінювати стан disabled для цього елемента.

    Враховуючи все вищесказане, нижче представлений код перевірки для зупинки анімації:

    for (let i = 0; i < el.length; i++) {
    el.addEventListener. (“click”, function() {
    // інший код
    // код зупинки анімації
    setTimeout(() => {
    isElementInViewport(firstItem) ? setBtnState(arrowPrev) : setBtnState(arrowPrev, false);
    isElementInViewport(lastItem) ? setBtnState(arrowNext) : setBtnState(arrowNext, false);
    }, 1100);
    // інший код
    });
    }

    Помітили, що перед виконанням цього коду варто затримка 1.1 секунди? Навіщо так робити? Якщо повернутися в CSS, там є це правило:

    .timeline ol {
    transition: all 1s;
    }

    Анімація таймлайна виконується за 1 секунду. Далі ми чекаємо 100 мілісекунд і виконуємо перевірки. Таймлайн з анімацією:

    Додаємо підтримку свайпов

    Поки що таймлайн ніяк не реагує на події торкань. Добре б додати цей функціонал. Можна написати свій JS-спосіб, а можна взяти готову бібліотеку (Hammer.js або TouchSwipe.js).

    Не будемо ускладнювати і для простоти візьмемо Hammer.js. Першим ділом додаємо бібліотеку у Pen:

    Створюємо горизонтальний таймлайн з допомогою CSS і JavaScript

    Потім оголошуємо функцію:

    function setSwipeFn(tl, prev, next) {
    const hammer = new Hammer(tl);
    hammer.on(“swipeleft”, () => next.click());
    hammer.on(“swiperight”, () => prev.click());
    }

    У функції зверху ми:

    створюємо об’єкт Hammer;

    реєструємо обробники для подій swipeleft і swiperight;

    коли ми робимо свайп ” вліво, ми викликаємо клік по кнопці вперед, і таймлайн зсувається праворуч наліво;

    коли ми робимо свайп вправо, ми викликаємо клік по кнопці назад, і таймлайн зсувається ліворуч направо.

    Таймлайн з підтримкою свайпов:

    Додаємо навігацію по клавіатурі

    Давайте покращимо UX і додамо підтримку навігації по клавіатурі. Наші цілі:

    При натисканні кнопки вліво і вправо документ повинен скролиться до верхньої позиції таймлайна (якщо інша секція сторінки зараз видима). Так весь таймлайн буде видно.

    При натисканні стрілки вліво таймлайн повинен анимироваться зліва направо.

    При натисканні стрілки вправо таймлайн повинен анимироваться справа наліво.

    Відповідна функція:

    function setKeyboardFn(prev, next) {
    document.addEventListener. (“натискання”, (e) => {
    if ((e.which === 37) || (e.which === 39)) {
    const timelineOfTop = timeline.offsetTop;
    const y = window.pageYOffset;
    if (timelineOfTop !== y) {
    window.scrollTo(0, timelineOfTop);
    }
    if (e.which === 37) {
    prev.click();
    } else if (e.which === 39) {
    next.click();
    }
    }
    });
    }

    Таймлайн з підтримкою клавіатури:

    Додаємо адаптивність

    Майже закінчили! Останній, але не менш важливий етап – давайте зробимо таймлайн адаптивним. Коли вьюпорт менше 600px, макет повинен перемикатися на такий стек:

    Створюємо горизонтальний таймлайн з допомогою CSS і JavaScript

    Так як ми використовуємо підхід desktop-first, нижче представлені CSS-правила, які необхідно переписати:

    @media screen and (max-width: 599px) {
    .timeline ol,
    .timeline ol li {
    width: auto;
    }
    .timeline ol {
    padding: 0;
    transform: none !important;
    }
    .timeline ol li {
    display: block;
    height: auto;
    background: transparent;
    }
    .timeline ol li:first-child {
    margin-top: 25px;
    }
    .timeline ol li:not(:first-child) {
    margin-left: auto;
    }
    .timeline ol li div {
    width: 94%;
    height: auto !important;
    margin: 0 auto 25px;
    }
    .timeline ol li:nth-child div {
    position: static;
    }
    .timeline ol li:nth-child(odd) div {
    transform: none;
    }
    .timeline ol li:nth-child(odd) div::before,
    .timeline ol li:nth-child(even) div::before {
    left: 50%;
    top: 100%;
    transform: translateX(-50%);
    border: none;
    border-left: 1px solid white;
    висота: 25px;
    }
    .timeline ol li:last-child,
    .timeline ol li:nth-last-child(2) div::before,
    .timeline ol li:not(:last-child)::after,
    .timeline .arrows {
    display: none;
    }
    }

    Замітка: у двох правила в коді зверху нам довелося використовувати !important, щоб переписати инлайновые стилі, застосовані через JS.

    Фінальний таймлайн:

    Підтримка в браузерах

    Демо добре працює у всіх останніх версіях браузерів і пристроїв. Також ви могли помітити, що ми компілювали наш ES6-код в ES5 з допомогою Babel.

    Єдина маленька проблема, з якою я зіткнувся при тестуванні, полягала в зміні тексту при анімуванні. Я випробував різні підходи з Stack Overflow, але не зміг знайти простого рішення для всіх операційних систем і браузерів. Так що знайте, що під час анімації можуть виникати невеликі проблеми візуалізації тексту.

    Висновок

    У цьому досить об’ємному уроці ми почали з простого нумерованого списку і закінчили адаптивним горизонтальним таймлайном. Можу з упевненістю сказати, що ми вивчили багато чого цікавого. Сподіваюся, вам сподобалася наша робота, і ви почерпнули з неї щось нове.