Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Новое в блоге: Анимация Skeleton

Как сделать простой scrollbar на Typescript

Филипп Алёхин
Web-разработчик

Часто в мире frontend’а разработчики по каким-либо причинам не могут использовать scrollbar, который предлагает браузер. Решают эту проблему обычно заменой стандартного скроллбара на кастомный. Однако и готовые решения не всегда могут помочь, например, если проблем связана с производительностью за счет эмуляции работы скролла. Не желая мириться с такой ситуацией, мы решили написать собственную реализацию скроллбара, которую в дальнейшем можно развивать и дальше.

 

Изначально мы поставили следующие требования:

  1. Нативный механизм скроллбара должен быть сохранен: меняется только его внешний вид.
  2. Решение должно быть независимым.
  3. Должна присутствовать возможность изменять внешний вид.

Устройство скроллбара

Перед тем, как сделать scrollbar разберемся с тем, из чего обычно состоит его стандартный механизм:

сделать простой scrollbar - блоки

Viewport – видимая нам часть content блока, также может называется window.

Content – содержимое блока, которое пользователь просматривает через viewport.

Slider – это то, что обычно называют scrollbar, состоит из track и grip которые описаны ниже.

Button – кнопки, которые обычно используются для перемещения вверх или вниз на одну абстрактную единицу.

Вместе slider и buttons образуют скроллбар.

сделать scrollbar - кнопки

Grip – элемент, который мы можем тянуть вверх и вниз для просмотра содержимого.

Track – место, в котором перемещается grip. Клик по track’у перемещает grip в место клика.

Реализация

Итак, после небольшой теоретической части приступим к реализации. Какие задачи нам нужно решить в первую очередь, чтобы сделать скроллбар?

  1. Во-первых, скрыть системный scrollbar.
  2. А во-вторых, отобразить кастомный.

Для решения первой задачи определим следующую html структуру:

<div class="basic-scrollsome-element">
  <div class="basic-scroll__viewport">
    <div class="content">
    </div>

    <div class="basic-scroll__grip">
    </div>
  </div>
</div>

где some-element – элемент, у которого мы хотим изменить скролл с шириной чуть меньшей, чем у viewport и со свойством overflow: hidden;viewport – блок, в который не вмещается по высоте content и со свойством overflow-y: scroll, что приводит к появлению системного скроллбара. К сожалению, чтобы узнать ширину скролла css недостаточно, для этого объявим класс BasicScroll.

class BasicScroll {

private element: HTMLElement;
private viewport: HTMLElement;
private grip: HTMLElement;

// В конструктор нашего класса приходит HTML элемент, на который нужно
// повесить скролл
// gripClass - css класс для нашего grip'а
constructor(element: HTMLElement, gripClass = 'opacity-grip') {

// Помещаем наш элемент в переменную и добавляем ему css класc basic-scroll
this.element = element;
this.element.classList.add('basic-scroll');

// Создаем viewport с классом basic-scroll__viewport
// и задаем ему ширину равную ширине элемента
this.viewport = document.createElement('div');
this.viewport.classList.add('basic-scroll__viewport');
this.viewport.style.width = this.element.clientWidth + 'px';

// Создаем grip и добавляем его во viewport
this.grip = document.createElement('div');
this.grip.classList.add('basic-scroll__grip', gripClass);
this.viewport.appendChild(this.grip);

// Копируем все содержимое элемента и помещаем его во viewport
for(let i = 0, l = this.element.children.length; i< l; i++) {
this.viewport.appendChild(this.element.children[i]);
        }
this.element.appendChild(this.viewport);

// Уменьшаем ширину элемента на ширина скролла
this.element.style.width = `${this.element.clientWidth - (this.viewport.offsetWidth - this.viewport.clientWidth)}px`;
}
}

Таким образом мы решили первую задачу – скрыли системный скролл.

Отображение grip’а

Опишем следующие методы:

// Вешаем обработчики событий на элементы
privatesetupEvents() {
this.viewport.addEventListener('scroll', () => {
this.update();
});

// При наведении курсора на viewpoint плавно будет показан grip
this.viewport.addEventListener('mouseover', () => {
this.showGrip();
        });

// Тоже самое, только наоборот
this.viewport.addEventListener('mouseleave', () => {
this.hideGrip();
});
    }

privateupdate() {
// Получаем высоту скролла и высоту viewport'а и их отношения
letscrollSize = this.content.scrollHeight;
letviewportSize = this.viewport.clientHeight;
let ratio = viewportSize / scrollSize;

letgripSize = ((ratio > 1) ? 1 : ratio) * viewportSize;
this.grip.style.height = gripSize + 'px';

// Высчитываем максимальную дистанцию, которую можно скролить
letmaxScrollSize = scrollSize - viewportSize;
letscrollRelative = this.viewport.scrollTop / maxScrollSize;

this.grip.style.top = (scrollRelative * (this.element.scrollHeight - gripSize)) + 'px';
}

В результате получим следующее:

scroll-2

Итог

На текущий момент решение позволяет использовать стандартный механизм скролла, где можно изменять внешний вид и не зависеть от других библиотек. При всем этом, файл весит всего 1.5 килобайта в сжатом виде.

К минусам можно отнести отсутствие горизонтального скролла, а также скудный функционал.

В будущем планируется развивать и улучшать это решение, весь код можно найти на Github.

Читать и комментировать

Краснодар

Коммунаров, 268,
3 эт, офисы 705, 707

+7 (861) 200 27 34

Хьюстон

3523 Brinton trails Ln Katy

+1 833 933 0204

Москва

+7 (495) 145-01-05