Прокрутка контента внутри блока на Javascrip.
Вступление.
При создании сайта очень часто возникает необходимость реализовать вертикальную прокрутку контента внутри блока <DIV>
. Многие из вас скажут, что нет ничего проще, достаточно установить фиксированную высоту блока и задать его свойству overflow-y
значение auto
или scroll
. Действительно, этого вполне достаточно, но, в большинстве случаев, полосы прокрутки нарушают дизайн веб-страницы.
В этой статье я расскажу вам, как сделать прокрутку контента в блоке и сам скроллбар c помощью JavaScript используя событиt scroll
. Подробно рассмотрим HTML-вёрстку, CSS и сам код на чистом JavaScript.
Составим техническое задание на создание прокрутки контента внутри блока:
- Прокрутка должна осуществляется колёсиком мыши при наведении на выбранный блок или перетаскиванием ползунка скроллбара.
- Блок должен иметь кастомный скроллбар с высотой ползунка, зависящей от размера контента внутри блока.
- При прокрутке контента в блоке использовать стандартное событие scroll.
- На странице может быть несколько блоков с прокруткой контента.
- Ширина и высота блоков с прокруткой не должна влиять на работу скрипта.
HTML-разметка блока с прокруткой.
HTML-код блока с прокруткой контента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<div class="container"> <!-- container --> <div class="viewport"> <!-- viewport --> <div class="content-box"> <!-- contentBox --> <ul class="content-list"> <li>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed efficitur nunc tincidunt lorem luctus, at venenatis enim lobortis.</li> <li>Duis varius augue a iaculis convallis. Vestibulum sodales varius metus, ac maximus est auctor vitae. </li> <li>Sed lacinia orci est, nec sollicitudin odio viverra sed. Pellentesque nec massa ac nulla dapibus ornare.</li> <li>Maecenas non leo eget tortor facilisis congue id sed justo. Vestibulum eget suscipit est, nec viverra urna.</li> <li>Quisque mollis pharetra risus id maximus. Integer sed pharetra diam.</li> <li>Aliquam erat volutpat. Nam elementum, ligula ut dapibus facilisis, orci quam pharetra mauris, sed tristique est nunc sit amet libero</li> <li>Ut vel tortor malesuada, tempus neque sed, ullamcorper ex.</li> <li>Maecenas volutpat congue rutrum. Quisque ornare, felis ut pharetra semper, sem lectus sollicitudin ante, vitae mattis nisl orci at massa.</li> <li>Praesent sollicitudin augue ac euismod rhoncus. Praesent sit amet tincidunt nulla. Phasellus tincidunt nec ipsum ut efficitur.</li> <li>Duis varius augue a iaculis convallis. Vestibulum sodales varius metus, ac maximus est auctor vitae. </li> <li>Sed lacinia orci est, nec sollicitudin odio viverra sed. Pellentesque nec massa ac nulla dapibus ornare.</li> </oul> </div> </div> </div> |
Кратко рассмотрим назначение каждого элемента:
- container
- Родительский элемент, задаёт ширину, высоту и дизайн блока с прокруткой контента.
- viewport
- Область просмотра, которая определяет границы контента, за которыми контент не виден. Кроме этого, вьюпорт обрезает и делает невидимой дефолтную полосу прокрутки контейнера
contentBox
. - contentBox
- Контейнер, в котором размещён и будет прокручиваться контент. Именно у этого контейнера стандартная полоса прокрутки будет заменена на кастомную.
Обратите внимание — показ дефолтной полосы прокрутки мы запретили, а HTML-вёрстку кастомного скроллбара не сделали. Он будет формироваться JS-скриптом в случае необходимости, т. е., если высота контента больше высоты контейнера contentBox
. Такое решение предоставляет большую гибкость в применении, отсутствует зависимость от высоты контента. Это удобно, когда на странице несколько таких блоков с разным наполнением контентом.
Таблица стилей для блока с прокруткой.
Ниже я приведу только те стили, которые непосредственно относятся к блоку с прокруткой контента.
Для уменьшения размера таблицы стилей, я не буду приводить свойства с вендорными префиксами, обеспечивающими кроссбраузерность. Не забывайте прописывать их при реализации своих проектов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
.container { width: 300px; height: 420px; border: solid 1px #dedede; border-radius: 2px; box-shadow: 0 2px 0 0 rgba(0,0,0,0.04); background: #fff; } .viewport { overflow: hidden; position: absolute; left: 19px; right: 9px; top: 7px; bottom: 7px; } .content-box { height: 100%; position: absolute; left: 0; right: -17px; overflow-y: scroll; } .content-list { padding: 0 20px 0 5px; user-select: none; } .content-list li + li { margin-top: 10px; } .scrollbar, .scroller { width: 4px; position: absolute; top: 0; } .scrollbar { right: 0; bottom: 0; } .scroller { left: 0; border-radius: 2px; background: #ccc; cursor: pointer; } |
Начинаем писать JavaScript для блока с прокруткой контента.
Первое, что мы сделаем, это создадим анонимную самозапускающуюся функцию, внутри которой и будет расположен наш код. Нужно взять за правило ограничивать область видимости скрипта, чтобы исключить конфликты с другими JS-скриптами подключенными к странице.
1 2 3 4 5 6 |
;(function() { 'use strict'; })(); |
При написании JS-скрипта мы будем использовать конструкцию Class. Это позволит создать несколько экземпляров блоков с прокруткой на одной странице.
Создание экземпляров блоков с прокруткой контента.
В-первую очередь, создадим коллекцию блоков, в которых может понадобиться прокрутка контента. Далее, с помощью метода for...of
переберём полученную коллекцию, при этом мы выполним следующие действия:
- создадим экземпляр текущего блока, используя конструктор класса
ScrollBox
; - создадим HTML-вёрстку скроллбара, если высота контента в текущем блоке больше высоты родительского контейнера.
- зарегистрируем обработчики событий.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
;(function() { 'use strict'; class ScrollBox { constructor(container) { } } // выбираем все блоки на странице, в которых может понадобиться // прокрутка контента const containers = document.querySelectorAll('.container'); // перебираем полученную коллекцию элементов for (let container of containers) { // создаём экземпляр контейнера, в котором будем прокручивать контент const scrollbox = new ScrollBox(container); } })(); |
Весь дальнейший JS-код мы будем писать внутри конструкции class ScrollBox { ... }
.
Прежде всего рассмотрим конструктор класса ScrollBox
. Конструктор инициализирует ряд объектов и переменных, содержащих информацию об экземпляре блока прокрутки. В качестве аргумента конструктор принимает объект блока с контентом, экземпляр которого создаётся в данный момент.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ScrollBox { constructor(container) { // область просмотра, в которой находится контент и скроллбар this.viewport = container.querySelector('.viewport'); // контейнер, в котором будет прокручиваться информация this.contentBox = container.querySelector('.content-box'); // флаг нажатия на левую кнопку мыши this.pressed = false; this.init(); } } |
Кроме проинициализированных переменных, нам необходимо добавить константу, являющуюся приватным статическим полем. В данной константе будет храниться минимальная высота ползунка полосы прокрутки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ScrollBox { // минимальная высота ползунка скроллбара static #SCROLLER_HEIGHT_MIN = 25; constructor(container) { // область просмотра, в которой находится контент и скроллбар this.viewport = container.querySelector('.viewport'); // контейнер, в котором будет прокручиваться информация this.contentBox = container.querySelector('.content-box'); // флаг нажатия на левую кнопку мыши this.pressed = false; this.init(); } } |
Конструктор, кроме инициализации объектов и переменных, вызывает функцию init()
. Алгоритм работы данной функции:
-
1
Сравнивает высоту контента с высотой вьюпорта. Если высота контента окажется меньше или равна высоте вьюпорта, то функция прекращает работу, т. к. необходимость в прокрутке отсутствует.
-
2
Определяет максимальную прокрутку контента.
-
3
Вычисляет соотношение между высотами вьюпорта и контента.
-
4
Вызывает функцию
createScrollbar()
для создания полосу прокрутки. -
5
Вызывает функцию
registerEventsHandler()
регистрирующую обработчики событий для управления прокруткой как вращением колёсика мыши, так и перетаскиванием ползунка скроллбара.
Рассмотрим код функции init()
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
init() { // высоты полученных элементов this.viewportHeight = this.viewport.offsetHeight; this.contentHeight = this.contentBox.scrollHeight; // если высота контента меньше или равна высоте вьюпорта, // выходим из функции if (this.viewportHeight >= this.contentHeight) return; // возможная максимальная прокрутка контента this.max = this.viewport.clientHeight - this.contentHeight; // соотношение между высотами вьюпорта и контента this.ratio = this.viewportHeight / this.contentHeight; // формируем полосу прокрутки и полунок this.createScrollbar(); // устанавливаем обработчики событий this.registerEventsHandler(); } |
Создание скроллбара для блока с прокруткой контента.
Как я писал выше, скроллбар и его ползунок формируются с помощью функции createScrollbar()
.
Рассмотрим алгоритм работы данной функции:
-
1
Используя встроенный в JavaScript метод
document.createElement
, создаёт новые DOM-элементыDIV
, из которых будут сформированы полоса прокрутки и ползунок. -
2
Присваивает созданным элементам классы —
scrollbar
иscroller
соответственно. -
3
Вставляет созданные элементы во вьюпорт.
-
4
Получает DOM-объект ползунка полосы прокрутки, вычисляет и устанавливает его высоту.
Высота ползунка зависит от соотношения высот вьюпорта и контента (коэффициент ratio
), при этом чем больше высота контента, тем меньше высота ползунка полосы прокрутки.
Код функции createScrollbar()
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
createScrollbar() { // создаём новые DOM-элементы DIV из которых будет // сформирован скроллбар const scrollbar = document.createElement('div'), scroller = document.createElement('div'); // присваиваем созданным элементам соответствующие классы scrollbar.className = 'scrollbar'; scroller.className = 'scroller'; // вставляем созданные элементы в document scrollbar.appendChild(scroller); this.viewport.appendChild(scrollbar); // получаем DOM-объект ползунка полосы прокрутки, вычисляем и // устанавливаем его высоту this.scroller = this.viewport.querySelector('.scroller'); this.scrollerHeight = parseInt(this.ratio * this.viewportHeight); this.scrollerHeight = (this.scrollerHeight <= ScrollBox.#SCROLLER_HEIGHT_MIN) ? ScrollBox.#SCROLLER_HEIGHT_MIN : this.scrollerHeight; this.scroller.style.height = this.scrollerHeight + 'px'; } |
Теперь необходимо зарегистрировать обработчики событий, которые будут управлять прокруткой контента внутри вьюпорта.
Регистрация обработчиков событий для блока с прокруткой контента.
Регистрация обработчиков событий осуществляется в функции registerEventsHandler()
, вызываемой из рассмотренной ранее функции init()
. Обработчик назначается вызовом метода addEventListener
, который привязывается к элементу, на котором событие регистрируется.
При использовании метода
addEventListener
теряется контекст вызова — this
, который ссылается на текущий объект. Используя встроенный в JavaScript метод bind
, можно напрямую передать контекст вызова в функцию обработчика события.
Регистрировать мы будем следующие события:
- scroll
- вращение колёсика мыши, при этом курсор должен быть над вьюпортом;
- mousedown
- нажатие на левую кнопку мыши, когда курсор находится на ползунке;
- mousemove
- перемещение мыши по полосе прокрутки и документу в целом, т. е., если при перемещении курсор покинет область полосы прокрутки, перемещение ползунка не прервётся;
- mouseup
- отпускание левой кнопки мыши, при этом курсор может находиться над любой частью документа.
Код функции registerEventsHandler()
и комментарии к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
registerEventsHandler(e) { // вращение колёсика мыши this.contentBox.addEventListener('scroll', () => { this.scroller.style.top = (this.contentBox.scrollTop * this.ratio) + 'px'; }); // нажатие на левую кнопку мыши this.scroller.addEventListener('mousedown', e => { // координата по оси Y нажатия левой кнопки мыши this.start = e.clientY; // устанавливаем флаг, информирующий о нажатии левой кнопки мыши this.pressed = true; }); // перемещение мыши document.addEventListener('mousemove', this.drop.bind(this)); // отпускание левой кнопки мыши document.addEventListener('mouseup', () => this.pressed = false); } |
Как видно из кода, событие mousedown
привязывается к элементу scroller
. А вот события mousemove
и mouseup
привязаны к объекту document
, т. е. событие может наступить при положении курсора над любой частью страницы. Такое поведение предусмотрено при дефолтной прокрутке контента внутри блока при назначении ему стиля overflow-y: scroll
. Логично, что я постарался сохранить его.
Управление прокруткой контента внутри блока по событию ‘scroll’.
При возникновении события scroll
прокрутка будет осуществляться стандартными методами встроенными в браузер. Нам потребуется только изменить положение ползунка.
Изменять положение ползунка будем меняя значение свойства top
пропорционально изменению значения текущей прокрутки контента с учётом, полученного ранее, коэффициента ratio
. Давайте ещё раз посмотрим на функцию, которая вызывается при наступлении события scroll
:
1 2 3 |
this.scroller.style.top = (this.contentBox.scrollTop * this.ratio) + 'px'; |
Далее я расскажу вам, как перетаскивается ползунок по полосе прокрутки и как меняется прокрутка контента в зависимости от текущего положения ползунка.
Прокрутка контента перемещением ползунка скроллбара.
Перетаскивание ползунка начинается с наступления события mousedown
. При обработке этого события запоминается координата по оси Y — e.clientY
, которая будет считаться точкой начала перемещения ползунка. Кроме этого, выставляется флаг сигнализирующий о нажатии левой кнопки мыши.
JS-код, реализующий это, находится в функции, являющейся вторым параметром регистратора обработчика события mousedown
. Напомню этот код, чтобы вам не листать страницу:
1 2 3 4 5 6 7 8 |
this.scroller.addEventListener('mousedown', e => { // координата по оси Y нажатия левой кнопки мыши this.start = e.clientY; // устанавливаем флаг, информирующий о нажатии левой кнопки мыши this.pressed = true; }); |
За перетаскивание ползунка отвечает функция drop
, которая вызывается при возникновении события mousemove
. Давайте рассмотрим алгоритм работы этой функции:
-
1
Проверяет установку флага нажатия левой кнопки мыши
pressed
. Если флаг не установлен, то прекращает свою работу. -
2
Вычисляет величину перемещения мыши, как разницу между координатой старта
this.start
и текущей координатой курсора по оси Y —e.clientY
. -
3
Изменяет положение бегунка на величину перемещения курсора, меняя его смещение по вертикали путём изменения CSS-свойства
top
. -
4
Ограничивает перемещение ползунка верхней и нижней границей вьюпорта.
-
5
Прокручивает контент на величину пропорциональную перемещению ползунка.
-
6
Устанавливает координату Y начала движения мыши равной текущей координате Y.
Прежде чем начнём писать код функции drop
, давайте подробнее разберём п. 4 представленного алгоритма.
Ранее нами было получено максимально возможное смещение ползунка от верхней границы вьюпорта — scrollerMaxOffset
, вычисленное как разность высоты вьюпорта и высоты ползунка. Давайте посмотрим на рисунок:
Из рисунка видно, что если попытаться ещё увеличить смещение ползунка, то его нижняя часть выйдет за пределы вьюпорта. Другими словами, сумма высоты ползунка и его смещения не должна превышать высоту вьюпорта.
Для того, чтобы ползунок не уходил за верхнюю границу вьюпорта, необходимо контролировать, чтобы его смещение от этой границы было больше или равно нулю.
Теперь можно представить полный код функции drop
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
drop(e) { e.preventDefault(); // если кнопка мыши не нажата, прекращаем работу функции if (this.pressed === false) return; // величина перемещения мыши let shiftScroller = this.start - e.clientY; // изменяем положение бегунка на величину перемещения мыши this.scroller.style.top = (this.scroller.offsetTop - shiftScroller) + 'px'; // величина, на которую должен переместиться контент let shiftContent = this.scroller.offsetTop / this.ratio; // сумма высоты ползунка и его отступа от верхней границы вьюпорта const totalheightScroller = this.scroller.offsetHeight + this.scroller.offsetTop; // максимальный отступ, который может быть у ползунка в зависимости от его // высоты и высоты вьюпорта const maxOffsetScroller = this.viewportHeight - this.scroller.offsetHeight; // ограничиваем перемещение ползунка // по верхней границе вьюпорта if (this.scroller.offsetTop < 0) this.scroller.style.top = '0px'; // по нижней границе вьюпорта if (totalheightScroller >= this.viewportHeight) this.scroller.style.top = maxOffsetScroller + 'px'; // прокручиваем контент на величину пропорциональную перемещению ползунка this.contentBox.scrollTo(0, shiftContent); // устанавливаем координату Y начала движения мыши равной текущей координате Y this.start = e.clientY; } |
При отпускании левой кнопки мыши, наступает событие mouseup
и перетаскивание ползунка скроллбара прекращается. При этом флаг pressed
получает значение false
.
Напомню этот код, чтобы вам не листать страницу:
1 2 3 |
document.addEventListener('mouseup', () => this.pressed = false); |
На этом изучение прокрутки контента в блоке можно считать законченным. Посмотреть пример различных вариантов прокрутки блока, скачать HTML-вёрстку и полный JS-код, Вы можете по ссылкам, указанным в начале страницы.
Комментарии
-
Комментарии должны содержать вопросы и дополнения по статье, ответы на вопросы других пользователей.
Комментарии содержащие обсуждение политики, будут безжалостно удаляться. -
Для удобства чтения Вашего кода, не забываейте его форматировать. Вы его можете подсветить код с помощью тега
<pre>
:
—<pre class="lang:xhtml">
- HTML;
—<pre class="lang:css">
- CSS;
—<pre class="lang:javascript">
- JavaScript. - Если что-то не понятно в статье, постарайтесь указать более конкретно, что именно не понятно.
-
-
Отдельно такой скрипт не писал, т. к. текущий легко, в основном, за счёт вёрстки и стилей, преобразуется в горизонтальный скролл блока. Хочу обратить особое внимание, что в этом случае нужно использовать событие мыши «wheel».
-
-
Можно архив не rar пожалуйста)
-
Залил zip-архив.
http://cleanjs.ru/wp-content/examples/scrollbox/scrollbox.zip
-
-
Спасибо автору за прекрасный урок.
Немного доработал код для поддержки события resize, но не уверен в правильности кода. Пока осваиваю премудрости js. Прошу вас указать на недостатки и, если возможно, предложить более продвинутый вариант. Из замеченных недостатков — скролл не удаляется, когда в нем нет необходимости. Помогите пожалуйста поправить.
В секцию fn.registerEventsHandler = function(e) {…
// document resize (добавил в самом низу)
// возможно ли отказаться от window, заменив его на document?
window.addEventListener(‘resize’, () => {
this.refreshScrollbar();
});
};ниже функции
fn.createScrollbar = function() {
…
};
добавил функцию
fn.refreshScrollbar = function() {
this.content = this.viewport.querySelector(‘.content’);
this.viewportHeight = this.viewport.offsetHeight;
this.contentHeight = this.content.scrollHeight;
// возможная максимальная прокрутка контента
this.max = this.viewport.clientHeight — this.contentHeight;
// соотношение между высотами вьюпорта и контента
this.ratio = this.viewportHeight / this.contentHeight;
// устанавливаем его высоту
this.scroller = this.viewport.querySelector(‘.scroller’);
this.scrollerHeight = parseInt(this.ratio * this.viewportHeight);
this.scrollerHeight = (this.scrollerHeight < this.scrollerHeightMin) ? this.scrollerHeightMin : this.scrollerHeight;
this.scroller.style.height = this.scrollerHeight + 'px';
// вычисляем максимально возможное смещение ползунка от верхней границы вьюпорта
// это смещение зависит от высоты вьюпорта и высоты самого ползунка
this.scrollerMaxOffset = this.viewportHeight — this.scroller.offsetHeight;
}; -
Также замечен недостаток, что при resize прячется индикатор прокрутки, если конента много и ранее была прокрутка вниз.
-
аффтор убей себя об стену. это двумя строчками делается, губошлёп.
-
1. Писать в стиле пАдонков уже не модно, так только школота прогуливающая бурсу делает.
2. Если ты имеешь ввиду overflow: hidden, то в результате будет огромный скроллбар, который нельзя задизайнить. Суть представленного скрипта, сделать кастомный скроллбар и с помощью него управлять скролом.
3. Следующее сообщение в таком стиле будет удалено, здесь сидят серьёзные люди, а не придурки, потолок которых «Hello world»
-
-
Спасибо, Вальдемар!
-
Добрый день, большое спасибо за крутой скролл. Только есть проблема, в мобильной версии скролл не срабатывает, подскажите пожалуйста как подправить. Спаибо
-
Для этого нужно зарегистрировать touch-события touchstart, touchmove, touchend и при их срабатывании вызывать функции, соответствующие событиям мыши mousedown, mousemove, mouseup.
-
-
Скроллер — фигня.
Если контент прокручиваемый контент будет очень большой, начнет заметно подтормаживать.
Не нужно придумывать прокрутку за браузер, у вас все-равно не выйдет его оптимизировать лучше, если только не будете весь свой контент на canvas рисовать.
А сказать браузеру, что содержимое элемента нужно прокрутить, можно изменяя свойство scrollTop и scrollLeft соответственно, привязав его к рукописному скроллеру.
И если вешаете событие при помощи addEventListener, особенно на документ, удаляйте его после использования removeEventListener, особенно если вешаете на такое событие как mousemove. -
Добрый день!
Вопрос со стороны автоматизации.
Можно ли такой блок скроллить как-то, например, скриптом?
Столкнулся с подобным на странице, есть блок, не факт, что такой, но похожий, внутри элемент, находящийся за видимой областью экрана, нужно его вытащить.
При использовании рядовых методов Selenium WebDriver (scrollIntoView, scrollTo) скроллится вся страница, но не блок -
А нельзя шаг прокрутки привязать к якорям? Чтобы прокрутка блоками выполнялась?
-
На эту тему есть статья «Плавная прокрутка страницы». Всё, что там описано подойдет и для отдельного блока.
-
А есть такое для горизонтального скролла?