- PVSM.RU - https://www.pvsm.ru -

Проблема размещения непрерывного контента произвольного объёма в экран, или окно, фиксированных размеров, существует несколько десятков лет. Примерно столько же существует и лучшее решение этой проблемы: элемент графического интерфейса — скроллбар.
Под катом можно узнать, как в ближайшее время будет работать скролл в 2ГИС-онлайн.
Механизм системного скролла реализуется на уровне базовых возможностей операционной системы, поэтому с уверенностью можно сказать, что он всегда лучше js-эмуляции: он производительнее, работает независимо от JavaScript и реализует все необходимые «фичи» системы для разного типа устройств.
Дизайн же системного скроллбара, особенно Windows младше 8 версии, способен изуродовать значительную часть сайтов интернета. То, что не все согласны с этим мириться, подтверждается фактом наличия большого числа решений, программно меняющих системный скроллбар на кастомный [1].
Сейчас в 2ГИС Онлайн [2] мы используем FleXcroll: он эмулирует механизм скролла и не подходит нам по ряду причин:
Все эти факторы заставили нас задуматься над двумя вопросами:
Нами были сформированы основные требования к решению, которое должно менять визуальное представление скроллбара:
На момент написания статьи, более или менее гибко кастомизировать скроллбар средствами CSS можно только в браузерах на движке webkit. Цвета скроллбара можно поменять в браузере Internet Explorer. В остальных браузерах поддержка кастомизации скроллбара через CSS полностью отсутствует. Отчасти это связано с жёсткой позицией w3c [3]:
There are just some things that CSS should not do, full stop.

Из того большого числа js-библиотек более половигы подменяют нативный механизм скролла. Это значит, что для враппера свойство overflow выставляется в значение hidden, а вложенный контейнер с нужным нам контентом меняет свою абсолютную позицию при генерации событий, связанных со скроллом (например, mousewheel). К таким решениям можно отнести: jScrollPane [4], Scrollbar Paper [5], jQuery Custom Scrollbar plugin [6], FleXcroll [7], Tiny Scrollbar [8] и многие другие.
При таком подходе возникает сразу два фундаментальных недостатка: отсутствие кроссбраузерности и отсутствие же кроссплатформенности. Дело в том, что интерфейс событий, генерируемых действиями пользователя, при помощи которых пользователь что-то скроллит, существенно отличается от браузера к браузеру: с точки зрения стандартов тут творится настоящий бардак. Более того, последовательность и логика «бросания» событий серьёзно отличается и между платформами. Например, трекпады на платформе Mac при скролле генерируют события типа Wheel с большей частотой, чем колесо обычной мыши, что приводит к чересчур быстрому скроллу [9] в ряде подобных решений.
Именно эти недостатки эмуляции скролла привели нас к формулированию первого пункта требований.
Многие решения изначально позиционируются как плагины к jQuery. В ситуации, когда мы используем jQuery по частям, возникает проблема экономии трафика. Проблема существенно растёт при наличии у плагина зависимости от куда более тяжеловесного jQuery UI. Это касается, например, ShortScroll и Vertical Scroll. А также от ряда других библиотек: например, один из немногих jQuery плагинов, сохраняющих нативный механизм скролла, Scrollbars, зависит от 4 плагинов: event.drag, resize, mousehold и mousewheel общим весом более 10 кб.
Третьему пункту требований не удовлетворяет ни одно из найденных нами решений.
У решения есть две основных задачи: 1) скрыть системный скроллбар и 2) отобразить кастомный скроллбар.
Для простоты будем рассматривать только вертикальный скроллбар — в нашем случае, как и в большинстве других, нужен только он. Кроме того, это экономит объём выходного кода. Для случая с горизонтальным скроллбаром рассуждения расширяются по аналогии.
Для начала, построим html структуру:
<div class='wrapper' id='wrapper'>
<div class='scroller' id='scroller'>
<article class='container' id='container'>
</article>
</div>
</div>
где container — собственно, то, что мы хотим скроллировать; scroller — блок, в который по высоте не помещается container, но у него выставлено свойство overflow-y: scroll, что приводит к появлению системного скроллбара у его правой границы; wrapper — окно с шириной чуть меньшей, чем у scroller и свойством overflow: hidden. Ширина меньше ровно на ширину скроллбара scroller.
К сожалению, средствами CSS невозможно точно узнать ширину системного скроллбара. Например, не работает вариант с выставлением ширины 125% для scroller и 80% для container, при котором, казалось бы, ширины container и wrapper должны точно совпасть. Можно сделать scroller заведомо шире, а wrapper и container выставить одинаковую ширину, но такой способ не подходит для резиновой вёрстки и порождает баг в webkit браузерах (см. ниже).
Введём js-переменные:
var wrapper = document.getElementById('wrapper'),
scroller = document.getElementById('scroller'),
container = document.getElementById('container');
Теперь мы можем вычислить ширину системного скроллбара: scroller.offsetWidth — это ширина scroller, включающая в себя border, padding, а также системный скроллбар. Если мы обнулим border и padding при помощи CSS, и вычтем scroller.clientWidth, мы получим искомую ширину скроллбара в пикселях.
В webkit-браузерах существует особенность, заставляющая скроллиться элементы при выделении в них текста в горизонтальном направлении, даже при overflow-x: hidden. То есть scroller начинает двигаться по горизонтали внутри меньшего по ширине wrapper, в результате чего обнажается скрытый нами системный скроллбар. К счастью, в webkit-браузерах, и только в них, мы можем обнулить ширину скроллбара средствами CSS, после чего ширины всех трёх блоков в точности совпадут и места для горизонтального скролла просто не будет:
.scroller::-webkit-scrollbar {
width: 0;
}
Теперь нарисуем и спозиционируем кастомный скроллбар. Для этого минимально усложним html структуру на 1 элемент, который и будет полностью отвечать за визуальное представление скроллбара:
<div class='wrapper' id='wrapper'>
<div class='scroller' id='scroller'>
<article class='container' id='container'>
</article>
<div class='scroller__bar'></div>
</div>
</div>
Здесь важно отметить, что для нашей задачи не требовалась прорисовка кнопок «вверх» и «вниз», а также подстилающего «трека». Впрочем, никаких ограничений для их реализации нет.
При реализации драга нарисованного кастомного скроллбара, главное — запретить выделение текста внутри скроллируемого контента. Для этого достаточно сделать вот такой бинд:
function dontStartSelect() {
return false;
}
function selection(on) {
if (on) {
$(document).on('selectstart', dontStartSelect);
} else {
$(document).off('selectstart', dontStartSelect);
}
}
event(bar, 'mousedown', function(e) {
e.preventDefault();
selection(false); // Disable text selection in IE8
});
event(document, 'mouseup blur', function() {
selection(true); // Enable text selection in IE8
});
Как видите, большая часть кода нужна для браузера IE8, который пока, к сожалению, нельзя сбрасывать со счетов. Обратите внимание, что сброс «нажатого» состояния мыши должен происходить не только при отпускании кнопки мыши, но и при потере страницей фокуса (blur).
Некоторые элементы внутри container всегда должны быть видны пользователю. Например, это могут быть заголовки разделов какой-то статьи, при клике на которые (это дополнительный функционал, который выходит за рамки решения) контент «перематывался» бы к кликнутому заголовку.
Ни абсолютное (относительно scroller), ни относительное (относительно своих начальных позиций) позиционирование заголовков в чистом виде, в данном случае не подходит. Первое — по причине схлапывания контента, окружающего заголовок и соответствующих им рывков при скролле; второе — по причине нативных неустранимых transition'ов у браузера Internet Explorer для скролла, которые приводят к дрожанию всех зафиксированных заголовков.
В связи с этим, html структуру пришлось ещё немного усложнить, обернув все заголовки в врапперы, задача которых — сохранять место под заголовками при их вырывании из потока во время фиксации абсолютным позиционированием. В принципе, аналогичного результата можно добиться подбором отрицательных margin'ов для заголовков и секций, к которым они относятся — тогда можно обойтись и без врапперов заголовков.
При каждом скролле для каждого заголовка необходимо проверять условие:
scroller.scrollTop - H[i].offsetTop > Sum(H[j].offsetHeight, j=0..i-1)
scroller.scrollTop - H[i].offsetTop < scroller.clientHeight - Sum(H[j].offsetHeight, j=i..n-1)
где H — массив элементов заголовков; i — номер заголовка, меняется в диапазоне от 0 до n-1; scroller.scrollTop — виртуальное расстояние от верхней границы container до верхней границы видимой части container; H[i].offsetTop — расстояние от верхней границы container до верхней границы заголовка H[i].
Выполнение обоих условий означает, что заголовок i находится где-то в видимой области, не конфликтует с другими заголовками по положению, и его фиксация не требуется. Нарушение первого условия означает, что заголовок пытается скрыться сверху, а второго — снизу. В обоих случаях требуется фиксация.
В webkit браузерах мы столкнулись с неприятным багом: событие mousewheel не пробрасывалось с фиксированных заголовков вверх к scroller. Это создавало эффект поломки (внезапного прекращения) скролла при попадании под курсор фиксированного заголовка (например, в результате всё того же скролла). То есть, пользователь крутит колесо мыши, под курсор попадает заголовок, фиксируется, и, внезапно, скролл перестаёт работать (точнее, скроллиться начинает вся страница).
К счастью, в webkit браузерах (а нам требовалось обратиться только к ним) есть такая возможность:
$(headers).on('mousewheel', function(e) {
var evt = document.createEvent('WheelEvent');
evt.initWebKitWheelEvent(e.originalEvent.wheelDeltaX, e.originalEvent.wheelDeltaY);
scroller.dispatchEvent(evt); // Пробрасываем событие на scroller
e.preventDefault(); // Останавливаем скролл страницы
});
Конечно, это сокращённая версия кода: необходимо проверять наличие соответствующих функций и типа события, чтоб не генерировать ошибки в других браузерах.
Для минимизации объёма js-кода и зависимостей был использован подход, при котором библиотека не является плагином jQuery, хотя по-умолчанию использует его.
Например, в самом простом случае (без фиксации заголовков), инициализация выглядит так:
acBar($('.wrapper'), {
scroller: '.scroller',
container: '.container',
bar: '.scroller__bar'
});
Причём $('.wrapper') может быть массивом html объектов — проинициализируется каждый из них.
Если вы хотите фиксации заголовков, ограничения верхнего положения скроллбара, и использования альтернатив jQuery, инициализация немного усложняется:
baron($('.test_advanced'), {
scroller: '.scroller',
container: '.container',
bar: '.scroller__bar',
barOnCls: '.scroller__bar_state_on', // Класс, навешиваемый скроллбару, когда он должен быть видимым
barTop: 40, // Ограничитель позиции скроллбара сверху
header: '.header__title',
hFixCls: 'header__title_state_fixed', // Класс, навешиваемый зафиксированным заголовкам
selector: qwery, // Селектор
event: function(elem, event, func, off) { // Менеджер событий
if (off) {
bean.off(elem, event, func);
} else {
bean.on(elem, event, func);
}
},
dom: bonzo // Библиотека для работы с DOM
});
Для менеджера событий пришлось сделать обёртку, поскольку его интерфейс отличается в разных библиотеках. В принципе, можно обернуть нативные функции типа addEventListener и отказаться от специализированного менеджера событий.
В тестировании принимали участие актуальные версии браузеров Chrome, Firefox, Opera, Safari и Internet Explorer (IE) на платформах Windows, Mac и iOs (тестировались, конечно, только существующие версии браузеров :)). Кроме этого, тестировался IE9 и IE8. Все тесты на всех браузерах проходят нормально.
Особенность предлагаемого в данной статье решения заключается в том, что даже в случае каких-то ошибок в JavaScript, скролл всё равно будет работать, поскольку он системный, поэтому категорию риска попадают лишь браузеры устаревших версий Android и Opera Mini, в которых системный скролл на элементах не реализован, или реализован плохо.
Предложенное решение позволяет сохранить механизм системного скролла, заменить его дизайн, полностью отказаться от жестких зависимостей от других библиотек, и уложиться в 998 байт сжатого кода (минифицированного и сжатого gzip). В довесок к этому, есть механизм фиксации каких-либо элементов скроллируемого контента (например, заголовков).
К недостаткам можно отнести отсутствие горизонтального скроллбара и контролов управления вертикальным.
Код решения можно скачать с Github [10].
Демо [11].
Внедрение данного решения запланировано на начало марта.
Автор: Diokuz
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/27458
Ссылки в тексте:
[1] кастомный: http://slodive.com/web-development/jquery-scroll/
[2] 2ГИС Онлайн: http://maps.2gis.ru
[3] позицией w3c: http://lists.w3.org/Archives/Public/www-style/2001Sep/0050.html
[4] jScrollPane: http://jscrollpane.kelvinluck.com/
[5] Scrollbar Paper: http://code.google.com/p/scrollbarpaper/
[6] jQuery Custom Scrollbar plugin: http://manos.malihu.gr/jquery-custom-content-scroller/
[7] FleXcroll: http://www.hesido.com/web.php?page=customscrollbar
[8] Tiny Scrollbar: http://baijs.nl/tinyscrollbar/
[9] чересчур быстрому скроллу: http://maps.2gis.ru/#/?history=project/moscow/center/37.596986%2C55.748488/zoom/11/state/firms/what/%D0%B1%D0%B0%D0%BD%D0%BA%D0%BE%D0%BC%D0%B0%D1%82/action/search/page/1/sort/relevance/rpage/1/ppage/1
[10] Github: https://github.com/Diokuz/baron
[11] Демо: http://goo.gl/HRwLp
[12] Источник: http://habrahabr.ru/post/169359/
Нажмите здесь для печати.