Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. JavaScript

Створення бездоганної каруселі. Частина 3

by
Read Time:12 minsLanguages:
This post is part of a series called Create the Perfect Carousel.
Create the Perfect Carousel, Part 2

Ukrainian (українська мова) translation by AlexBioJS (you can also view the original English article)

Це третя та остання частина серії посібників «Створення бездоганної каруселі». У першій частині ми оцінили каруселі на сайтах Netflix та Amazon, дві з найактивніше використовуваних каруселей у світі. Ми створили карусель та реалізували можливість її прокручування за допомогою дотиків пальцями.

Потім у другій частині ми додали можливість прокручування контенту по горизонталі, пагінації та індикатор ходу процесу. Гоп, і готово.

Тепер, в останній частині серії, ми зазирнемо у складний для розуміння та часто забуваний світ можливості керування каруселлю за допомогою клавіатури. Ми внесемо виправлення до коду для виконання перерахунку розміру каруселі при зміні розміру вікна перегляду (* видима для користувача область веб-сторінки. Тут і надалі примітка перекладача). І, нарешті, ми додамо декілька останніх штрихів за допомогою ефекту зусилля пружини.

Ви можете освіжити у пам'яті, де ми зупинилися, відвідавши цей Pen (* фрагмент коду на Codepen).

Можливість керування каруселлю за допомогою клавіатури

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

Хороша новина полягає в тому, що ц можливість звичайно легко реалізувати! Насправді браузери виконують більшу частину роботи за нас. Серйозно: спробуйте попереходити по елементам створеної нами каруселі за допомогою клавіши табуляції. Оскільки ми скористалися смисловою HTML-розміткою (* коли самі елементи певною мірою підказують, що за контент вони містять), то ви вже можете!

Проте ви помітите, що наші навігаційні кнопки пропадають. Це так, оскільки браузер не дозволяє сфокусуватися на елементі, який розташовується за межами нашого вікна перегляду. Тому, незважаючи на те що ми задали правило overflow: hidden, ми не можемо прокрутити карусель по горизонталі; інакше вона і справді буде прокручуватися для показу елемента у фокусі.

Це нормально, і таке рішення вважалося би, на мій погляд, «працездатним», хоча і не зовсім чудовим.

Карусель на Netflix працює таким же чином. Проте оскільки більшість їх заголовків довантажуються за потреби (* по ходу прокручування користувачем сторінки), а також є пасивна можливість їх перегляду за допомогою клавіатури (тобто вони не писали спеціально ніякого коду для реалізації цієї можливості), ми не можемо власне отримати ніяких заголовків за виключенням тих, що вже завантажились. Це також виглядає жахливо:

Keyboard AccessibilityKeyboard AccessibilityKeyboard Accessibility

Ми можемо зробити краще.

Оброблення події focus

Для цього ми будемо прослуховувати подію focus, що виникає при фокусуванні будь-якого елемента каруселі. При попаданні елемента у фокус ми запросимо у нього його позицію. Потім ми звіримо це значення зі sliderX та sliderVisibleWidth, щоб дізнатися, чи знаходиться той елемент у межах вікна перегляду. Якщо ні, то ми перейдемо до нього за допомогою того самого коду, що написали у частині 2.

У кінці функції carousel додайте наступний обробник подій:

Ви помітите, що ми передали третій аргумент, true. Замість призначення обробника подій кожному елементу ми можемо скористатися прийомом розробки під назвою делегування подій, щоб прослуховувати події, що виникають тільки для одного елемента, їх прямого предка. Подія focus не спливає, тому за допомогою true обробникові дається вказівка прослуховувати подію на стадії перехоплення, стадії, коли подія виникає для кожного елемента, починаючи з window до цільового елемента (у нашому випадку це елемент, що потрапив у фокус).

Вище нашого зростаючого набору обробників додайте функцію onFocus:

Ми будемо працювати у цій функції до кінця поточного розділу.

Нам потрібно виміряти відступи лівої та правої сторін елемента та перевірити, чи лежить будь-яке з цих значень за межами поточної області перегляду.

Ми отримуємо цільовий елемент за допомогою властивості target події і можемо виміряти його завдяки getBoundingClientRect:

Значення left та right задано відносно вікна перегляду, а не каруселі. Так що нам потрібно отримати відступ лівої сторони контейнера каруселі, щоб врахувати це. У нашому прикладі це буде 0, але для того щоб карусель була надійною, потрібно, щоб враховувалося те, що вона може бути поміщена де завгодно.

Далі ми можемо просто перевірити, чи розташовується елемент за межами видимої області каруселі, та перейти у тому напрямку:

Тепер, при натисненні клавіши табуляції, у каруселі чітко відбувається перехід по елементам за допомогою нашого клавіатурного фокусування (* стан вікна або елемента зображення, при якому вони приймають дані з клавіатури)! Всього-на-всього декілька рядків коду для вияву додаткової турботи про користувачів.

Перерахунок розміру каруселі

Ви, ймовірно, помітили по ходу роботи з посібником, що при зміні розміру вікна перегляду вашого браузера перехід по елементами каруселі вже не працює належним чином. Це так, оскільки ми виміряли ширину каруселі відносно її видимої області лише один раз, у момент ініціалізації (* встановлення відомого початкового стану, наприклад, присвоєння початкових значень змінним. У деяких мовах програмування це робиться автоматично, іноді ініціалізація може бути об'єднана з оголошенням змінної).

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

Тепер, недалеко від початку нашої функції carousel, одразу після рядка, де ми визначаємо progressBar, нам потрібно замінити три з цих результатів обчислень розмірів, що зберігаються у константах, оголошених за допомогою const, на значення, що зберігаються у змінних, оголошених  за допомогою let, оскільки вони будуть змінюватися при зміні вікна перегляду:

Потім ми можемо перемістити логіку, за допомогою якої раніше обчислювалися ці значення розмірів, на нову функцію під назвою measureCarousel:

Нам потрібно одразу ж викликати цю функцію так, щоб ці значення як і раніше задавалися при ініціалізації: Прямо у наступному рядку викличте measureCarousel:

Карусель повинна працювати таким же чином, як і раніше. Для оновлення результатів обчислень розмірів при зміні розмірів вікна ми просто додаємо наступний обробник подій у самому кінці функції carousel:

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

Примітка з приводу продуктивності

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

Як ми коротко розглянули у часті 2, нерозумно виконувати ресурсозатратні обчислення частіше, ніж це потрібно. Ми сказали, що у випадку подій, які виникають при прокручуванні та діях з мишкою, вам потрібно, щоб вони виконувалися один раз за фрейм для підтримання частоти зміни кадрів (* швидкість сканування або виводу на екран відеокадрів (ЗО кадрів за секунду в стандарті NTSC і 25 кадрів за секунду в стандарті PAL/SECAM)) на рівні 60 кадрів за секунду. Події, що виникають при зміні розмірів вікна, трохи відрізняються тим, що при їх виникненні розміри елементів всього документа будуть повторно розраховані та деякі елементи перерозподіляться, що являє собою, ймовірно, найбільш ресурсозатратний момент, який може статися з веб-сторінкою.

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

За допомогою функції debounce по суті повідомляється наступне: «Запускати цю функцію тільки тоді, якщо вона не викликалася впродовж x мілісекунд». Ви можете звернутися за додатковою інформацією про debounce до чудового посібника для початківців, написаного David Walsh, а також ознайомитися там з деякими прикладами коду.

Останні штрихи

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

Проте це не серія посібників під назвою «Створення доволі хорошої каруселі". Прийшов час трохи покрасуватися, і для цього ми маємо приховану зброю. Ефекти зусилля пружин.

Ми додамо два таких ефекти. Один для випадків, коли каруселлю керують за допомогою дотиків пальцями, а інший для випадків, коли каруселлю керують за допомогою пагінації. Вони обидва дозволяють користувачам зрозуміти у веселій та захоплюючій манері, що вони досягли кінця каруселі.

Ефект зусилля пружини при дотиках пальцями

Для початку давайте додамо смикання у стилі iOS для випадків, коли користувач намагається прокрутити карусель за її межі. У цей момент ми задаємо межі прокручування за допомогою дотиків завдяки clampXOffset. Давайте замінимо її деяким кодом, завдяки якому додається смикання при виході значення розрахованого відступу за межі границь каруселі.

Для початку нам потрібно імпортувати перетворювач для створення нашої пружини. Є перетворювач під назвою nonlinearSpring, завдяки якому до позиції, заданої за допомогою наданого йому нами числа, застосовується експоненціально зростаюча сила у напрямку до origin (* від англ. початок). Це означає, що чим дальше ми тягнемо карусель, тим швидше вона відскочить назад. Ми можемо імпортувати його наступним чином:

У функції determineDragDirection ми маємо наступний код:

Одразу вище нього давайте створимо наші дві пружини, по одній для кожної межі прокручування каруселі:

Значення elasticity (* від англ. еластичності) визначається шляхом експериментування та вибору того, що вам підходить. Задайте його занадто низьким, і пружина буде здаватися занадто тугою. Задайте його занадто високим, ы ви не помітите її відскоку, або ще гірше, вона відтягне карусель ще далі від пальця користувача!

Тепер нам всього-на-всього потрібно написати просту функцію, за допомогою якої буде застосовано одну з цих двох пружин, якщо передане значення знаходиться за межами припустимого діапазону:

Ми можемо замінити clampXOffset у коді вище на applySpring. Тепер, якщо ви відтягнете карусель за її межі, то вона відскочить назад!

Але коли ми відпускаємо пружину, то вона неначе просто відскакує у вихідне положення. Нам потрібно внести зміни до функції stopTouchScroll, за допомогою якої реалізується можливість інерціального прокручування, для перевірки того, чи знаходиться карусель як і раніше за межами припустимого діапазону, і, якщо це так, застосувати замість раніше доданої пружину, що реалізується за допомогою дії physics (* кожна анімація Popmotion є дією).

Ефект зусилля пружини, який реалізується за допомогою physics

За допомогою дії physics також можна симулювати пружини. Нам лише потрібно передати цій функції в об'єкті властивості spring та to.

У stopTouchScroll перемістіть наявний код для ініціалізації функції physics, завдяки якій реалізується можливість прокручування контенту за допомогою дотиків, до частини логіки, за допомогою якої гарантується, що ми знаходимося у межах прокручування:

Ми знаємо, що у першій гілці інструкції if виконується код для випадків, коли карусель знаходиться за межами границь прокручування, так що ми можемо додати нашу пружину:

Ми хочемо створити ефект зусилля тугої та чутливої пружини. Я вибрав відносно високе значення spring для отримання різкого «клацання» та понизив значення friction до 0.92, щоб додати невеликий відскок. Ви могли би задати в якості значення тертя 1, щоб зовсім усунути відскок.

В якості невеликого домашнього завдання спробуйте замінити clampXOffset у методі output об'єкта physics, завдяки якому реалізується можливість прокручування контенту за допомогою дотиків, на функцію, за допомогою якої додається подібний ефект пружини, коли відступ по осі Х досягає меж каруселі. Замість наявної наразі різкої зупинки спробуйте зробити так, щоб вона зм'якшувалася наприкінці.

Ефект зусилля пружини при пагінації

Власникам пристроїв, керованих за допомогою дотиків, завжди надається крутий ефект зусилля пружини, вірно? Давайте потурбуємося і про користувачів настільних комп'ютерів, визначаючи момент, коли карусель знаходиться біля меж свого прокручування, і додаючи підказуюче смикання, щоб чітко та ясно показати користувачам, що вони знаходяться у кінці.

По-перше, нам потрібно зробити так, щоб кнопки для пагінації ставали неактивними по досягненні меж прокручування. Для початку давайте додамо правило CSS, за допомогою якого для кнопок задається спеціальне стильове оформлення, щоб показати, що вони неактивні. У правилі для button додайте:

Ми використовуємо тут клас замість більш смислового атрибута disabled, оскільки нам як і раніше потрібно, щоб спрацьовували обробники події click, чому, як підказує ім'я, disabled завадило би.

Додайте цей клас disabled кнопці Prev, оскільки початково відступ кожної каруселі має значення 0:

Ближче до верхньої частини функції carousel створіть нову функцію під назвою checkNavButtonStatus. Нам потрібно, щоб за допомогою цієї функції передане значення просто порівнювалося з minXOffset та maxXOffset і відповідним чином додавався клас disabled кнопки:

Заманливо було би викликати її кожного разу при зміні значення sliderX. У цьому випадку кнопки почали би блимати кожного разу при коливанні пружини біля границь прокручування. Також це би привело до чудної поведінки каруселі, якщо би була натиснута одна з кнопок під час одного з тих анімаційних ефектів зусилля пружини. Смикання «кінця прокручування» повинно завжди запускатися при досягненні кінця каруселі, навіть якщо виконується анімаційний ефект зусилля пружини, через який вона відтягується від абсолютного кінця.

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

В останньому рядку onWheel додайте checkNavButtonStatus(newX);.

В останньому рядку goto додайте checkNavButtonStatus(targetX);.

І, нарешті, у кінці determineDragDirection та гілці коду для додання можливості інерціального прокручування (код, що знаходиться у блоці else) функції stopTouchScroll замініть:

На:

Тепер все, що нам залишилося, – додати до функції gotoPrev та gotoNext код для перевірки наявності класу disabled у classList кнопок, за допомогою яких вони запускаються, і виконати пагінацію тільки у тому випадку, якщо він відсутній:

За допомогою функції notifyEnd створюється просто ще одна пружина, яка реалізується за допомогою physics, і вона виглядає наступним чином:

Поекспериментуйте з нею і, знов-таки, підгоніть значення аргументів physics, як вам потрібно.

Залишився тільки один невеликий баг. При відтягуванні каруселі за межі крайньої лівої границі індикатор ходу процесу повертається у зворотному напрямку. Ми можемо це швидко виправити, замінивши:

На:

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

Прибираємо за собою

У випадку односторінкових додатків веб-контент залишається довше у сесії користувача (* у порівнянні зі звичайними). Часто буває так, що при зміні сторінки як і раніше залишається запущеним те саме середовище виконання JS, що й при початковому завантаженні. Ми не можемо розраховувати на наявність чистого стану при кожному переході користувача за посиланням, і це означає, що ми повинні прибрати за собою, щоб запобігти спрацьовуванню обробників подій, зареєстрованих для мертвих елементів.

У React код для цього поміщено до методу componentWillLeave. У Vue з цією метою використовується beforeDestroy. У цій серії посібників ми реалізуємо карусель тільки за допомогою JS, проте ми як і раніше можемо надати метод для відв'язування обробників, який працював би подібним чином у будь-якому фреймворці.

Наразі функція carousel нічого не повернула. Давайте це змінимо.

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

Ми повернемо з carousel тільки функцію, за допомогою якої відв'язуються всі обробники подій. У самому кінці функції carousel додайте наступний код:

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

І на цьому все!

Фух-х-х. Ми з багато чим розібралися! Що в нас вийшло. Ви можете ознайомитися з остаточним результатом, відвідавши цей Pen на Codepen. У цій останній частині ми додали можливість керування каруселлю за допомогою клавіатури, можливість перерахунку розміру каруселі при зміні розміру вікна перегляду, декілька прикольних доповнень, реалізованих за допомогою ефекту зусилля пружини, та прикрий, але обов'язковий етап зруйнування всього створеного.

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

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.