Содержание
Статья посвещена решению проблемы кастомизации скроллбаров браузера ради воплощения в жизнь амбициозных идей дизайнера. Статья расчитана на тех, кто свободно ориентируется в технологиях html, css, js, т.к. предлагаемое решение основано на их компромиссном использовании.
В статье будут описаны и решены следующие задачи и цели:
- минимум JavaScript вычислений при прокрутке и изменении размеров элемента
- кроссбраузерность и работа на мобильных браузерах
- простота использования, кастомизации и внедрения
- учитывание поведения элементов при прокрутке с помощью выделения контента
- обновление параметров скроллбаров при обновлении, изменении или догрузке контента
- обход стандартного поведения содержимого браузера при изменении ширины его окна
Intro. Возможности системного скрола.
Чтобы было от чего отталкиваться, я приведу простой пример (посмотреть в работе):
<div style="border: 3px solid #f08080; height: 300px; overflow: auto; width: 300px;"> <div style="background-color: #90ee90; border: 10px solid #4169e1; min-height: 600px; min-width: 600px;"> content... </div> </div>
Браузер для навигации по контенту в такой области предоставляет следующие возможности (советую попробовать все из них):
- колесо мыши (некоторые мыши кроме вертикальной прокрутки предоставляют возможность горизонтальной прокрутки)
- клавиши-стрелки при фокусе на элементе (вверх, вниз, вправо, влево)
- тачпад (особенные удобства и кайф от манипулирования контентом можно ощутить на тачпадах у компьютеров Macintosh)
- тачскрин планшетов и телефонов, а также и компьютеров
- также можно прокрутить контент, выделяя его (такая возможность предусмотрена для выделения, например, больших текстов, часть которых находится за пределами видимости)
- и, непосредственно, элементы скроллбаров: ползунки и кнопки
Как можно заметить, набор возможностей весьма большой и он весьма хорошо реализован, продуман, удобен и привычен для конечного пользователя…
Стоит ли это все или часть этого реализовывать средствами js, чтобы изменить внешний вид скроллбаров?
да: потому что есть новые идеи для навигации по контенту, тяга к экспериментам с js и т.д.
нет: браузер уже все это делает и нужно найти способ, как это использовать. Вот об этом я и расскажу ниже.
Итак.
Решение имеет следующую структуру на html:
<div class="scrollar"> <div class="scrollar-viewport"> <div class="scrollar-systemscrolls"> <div class="scrollar-contentwrap"> <div class="scrollar-content"></div> </div> </div> </div> <div class="scrollar-scroll scrollar-vscroll"> <div class="scrollar-btn scrollar-btnup"></div> <div class="scrollar-btn scrollar-btndown"></div> <div class="scrollar-track scrollar-vtrack"> <div class="scrollar-thumb scrollar-vthumb"></div> </div> </div> <div class="scrollar-scroll scrollar-hscroll"> <div class="scrollar-btn scrollar-btnleft"></div> <div class="scrollar-btn scrollar-btnright"></div> <div class="scrollar-track scrollar-htrack"> <div class="scrollar-thumb scrollar-hthumb"></div> </div> </div> <div class="scrollar-corner"></div> </div>
Scrollar — одновременно название решения и namespace, чтобы исключить пересечение стилей на целевой странице, где решение может быть использовано.
Чтобы было понятнее — поясню структуру:
- корневой блок является оберткой для всей конструкции и определяет область и размеры всей конструкции в целом;
- viewport — этим блоком ограничена область просмотра контента;
- systemscrolls — этот блок отвечает за прокрутку контента;
- contentwrap — это корректирующий блок (о его смысле и возможностях ниже);
- content — непосредственно сам контент;
- vtrack и htrack — вертикальный и горизонтальный скроллбары. С их содержимым и так все понятно.
А теперь можно приступить к рассмотрению ключевых моментов…
Основная тяжесть: операции Scroll и Resize
На мой взгляд, это самые «тяжелые» и неудобные в реализации на javascript операции. Почему? Чтобы программно реализовать Resize и сохранение пропорций нужно, в основном, обрабатывать событие window.onresize, а во время возникновения этого события — корректировать размеры и пропорции у нескольких элементов (чаще всего так…). Недостатком ресайза подобным образом является неплавное (с небольшими непостоянными, заметными глазу, шагами) изменение размеров элемента, кто пытался подобное отладить — поймет меня. Это сильно «напрягает» глаза, когда вкладываешь душу в разработку и стараешься довести работу всего интерфейса до идела.
Таким образом, чтобы сохранить максимальную плавность при изменении размеров элементов стоит использовать один из вариантов резиновой верстки блочных элементов с абсолютным позиционированием относительно друг друга и изменением размеров элементов за счет привязки к определенным координатам:
.scrollar, .scrollar-viewport, .scrollar-systemscrolls, .scrollar-contentwrap, .scrollar-content { bottom: 0px; height: auto; left: 0px; position: absolute; right: 0px; top: 0px; width: auto; } /* корректировка стиля корневого элемента */ .scrollar { overflow: visible; position: relative; } /* эти блоки не должны иметь привязки справа и снизу */ .scrollar-contentwrap, .scrollar-content { bottom: auto; right: auto; }
после объявления таких стилей браузер будет сам изменять размеры как области прокрутки, так и внутренних элементов. Ниже по статье, стили некоторых элементов будут дополнены и скорректированы, чтобы добиться нужного и лучшего результата.
О Scroll. Для реализации scroll на desktop-браузерах необходимо обрабатывать событие колеса мыши и анализировать значения от этого события (также не забывать, что некоторые мыши позволяют листать контент в горизонтальной плоскости, а не только в вертикальной), а для mobile-браузеров нужно обрабатывать события группы touch. Т.е. для кроссплатформенного решения нужно программировать две эти реализации. Но лучше прокрутку контента возложить на сам браузер. Достаточно определить стиль для элемента systemscrolls:
.scrollar-systemscrolls { overflow: scroll; }
Скрытие системных скроллбаров и 22px
Решая задачу прокрутки контента, я использовал свойство overflow:scroll, которое заставляет браузер отображать скроллбары всегда и тем самым предоставляет пользователю все удобства системной прокрутки. Но теперь нужно эти скроллбары скрыть. Здесь как раз и выручит viewport — этот блок будет скрывать всё, что выходит за его пределы. Это можно сделать двумя способами:
.scrollar-viewport { overflow: scroll; } .scrollar-viewport { clip: rect(0, auto, auto, 0); clip: rect(0 100% 100% 0); }
Первый вариант с overflow прост для понимания, но когда пользователь захочет выделить контент и начнет тянуть курсор в нужную сторону, то он, вполне вероятно, увидит системные скроллбары, т.к. при таком действии они вылезут из-под скрываемой области. Вариант с clip таким не страдает, но в этом случае пришлось применить небольшой хак и для поддержки ie7. Но это ещё не всё… Блок systemscrolls имеет такие же размеры, как и блок viewport, т.е. системные скроллбары еще видны. Здесь и используется ключевой момент «22px» — это величина, на которую будет скорректирован блок systemscrolls. Дело в том, что толщина скроллбаров у популярных браузеров менее 21px. Сама корректировка выглядит так:
.scrollar-systemscrolls { bottom: -22px; right: -22px; }
После этого скроллбары будут скрыты и будут находиться за границами той области, которая обрезана с помощью clip.
И что в итоге? Браузер сам изменяет и следит за размерами всего элемента, контент легко и плавно прокручивается всеми описанными выше способами, а системные скроллбары скрыты. Но если это оставить в таком виде, то часть контента справа и снизу отображаться не будет…
Блок contentwrap
Основное и главное назначение блока contentwrap — это сделать так, чтобы в блоке viewport можно было увидеть блок content полностью: от одного края до другого при разных способах прокрутки.
До этого момента javascript не требовался, но сейчас он пригодится для того, чтобы скорректировать размеры блока contentwrap.
… var viewport = $(".scrollar-viewport", scrollar); var systemscrolls = $(".scrollar-systemscrolls", scrollar); var correct_h = systemscrolls[0].clientHeight - viewport.height(); // корректировка по высоте var correct_w = systemscrolls[0].clientWidth - viewport.width(); // корректировка по ширине …
таким образом, размеры элемента contentwrap будут получаться из сложения этих величин с размерами блока content, и делать это будет нужно при каждом изменении размеров блока content. Есть исключения, но о них будет рассказано ниже.
Корректировка блока contentwrap с помощью js позволяет не обращать внимания на вид и версию браузера и используемую им толщину скроллбаров.
Блоки vscroll и hscroll
vscroll и hscroll — скроллбары. На данный момент, основная задача — «приклеить» их к краям и заставить их изменять свои размеры и местоположение их дочерних элементов за счет браузера:
.scrollar-scroll, .scrollar-track, .scrollar-btn, .scrollar-thumb, .scrollar-corner { position: absolute; } .scrollar-hscroll { bottom: 0px; height: 0px; left: 0px; right: 0px; } .scrollar-vscroll { bottom: 0px; right: 0px; top: 0px; width: 0px; } .scrollar-btnup { left: 0px; top: 0px; } .scrollar-btndown { bottom: 0px; left: 0px; } .scrollar-btnleft { left: 0px; top: 0px; } .scrollar-btnright { right: 0px; top: 0px; } .scrollar-vthumb { max-height: 100%; height: 30px; left: 0px; right: 0px; } .scrollar-hthumb { max-width: 100%; width: 30px; bottom: 0px; top: 0px; }
в этом листинге нет ничего сложного и я перейду к более интересной части: бегунки.
Бегунки
Для успешной реализации функционала бегунков нужно рассмотреть следующие задачи:
- изменение положения бегунка при прокрутке контента указанными выше способами
- перетаскивание бегунка указателем мыши и реакция контента на эти действия
- изменение размеров и сохранение позиции бегунка при изменении размеров контента относительно размеров компонента или при изменении размеров компонента (эта задача решается при обновлении параметров всего компонента и решение будет описано ниже)
Изменение положения бегунка при прокрутке контента
Это сделать крайне просто. Благодаря установленному свойству overflow:scroll у блока systemscrolls можно ловить собитие scroll на этом блоке, а уже при возникновении этого события двигать бегунок в зависимости от положения (свойства scrollLeft и scrollTop) контента относительно левой верхней точки блока systemscrolls с учетом коэффициента пропорциональности, который вычисляется в функции обновления параметров компонента (об этом будет ниже).
Перетаскивание бегунков
Бегунок должен реагировать на поведение указателя мыши также, как и в системе. Сделать это весьма не сложно. Алгоритм заключается в следующем:
- поймать событие mousedown на самом бегунке
- подключить события mousemove и mouseup на элемент document
- обрабатывать событие document.mousemove, тем самым изменяя положение бегунка и пролистывая контент
- поймав событие document.mouseup — отключить события mousemove и mouseup на элемента document
Как можно заметить, алгоритм весьма простой, но есть один нюанс: изменение положения бегунков, точнее алгоритм изменения этого положения. Менять положение бегунков можно двумя способами:
- событие scroll от элемента systemscrolls. Оно возникает следующим образом: чтобы пролистывать контент с помощью js, нужно изменять свойства scrollLeft и scrollTop у блока systemscrolls, а изменение этих свойств приведет к выбросу события scroll у этого блога. Таким образом сработает алгоритм по изменению положения бегунка при прокрутке контента, который описан выше.
- после того, как будет поймано событие mousedown на бегунке, нужно отключить обработку события scroll на элементе systemscrolls, а элементы бегунков двигать по событию mousemove на элементе document одновременно пролистывая контент изменением свойств scrollLeft и scrollTop у блока systemscrolls.
На первый взгляд, первый способ кажется лучше, т.к. требует меньше усилий для реализации. Но недостатком этого способа является присутствующая обратная связь через событие systemscrolls.scroll (см. рисунок), из-за которой бегунок начинает заметно отставать от указателя мыши при быстром перемещении последнего. При этом, событие systemscrolls.scroll возникает всякий раз при изменении свойств scrollLeft и scrollTop у блока systemscrolls, вызывая функцию перемещения бегунков.
При втором способе можно получить гораздо лучший результат. Дополнительные операции по отсоединению и присоединению события systemscrolls.scroll происходят только два раза: на события mousemove и mouseup (соответственно) элемента document. Таким образом, обработка события document.mousemove происходить быстрее и оптимальнее (см. рисунок)
Обновление параметров компонента
Вот и дошло время до весьма важной функции — обновление параметров компонента. Для этой функции необходимо обеспечить скорость выполнения, т.к. она может вызываться при ресайзе и смене контента очень часто, поэтому большая часть её написана на чистом js. Ниже приведен кусок кода для обновления параметров по горизонтали:
// Горизонтальный скрол if (options.hscroll) { scroll = env.hscroll; var ss_cw = ss.clientWidth; // Корректировка ширины contentwrap cw.style.width = (ct.offsetWidth + env.correct_w) + "px"; while (Math.abs(cw.scrollWidth - ct.offsetWidth) <= 1) { cw.style.width = (ct.offsetWidth + 1000) + "px"; cw.style.width = (ct.offsetWidth + env.correct_w) + "px"; } var ss_sw = ss.scrollWidth; // Ширина горизонтального ползунка var htrack_w = scroll.track.width(); var hthumb_w = htrack_w * ss_cw / ss_sw; if (hthumb_w > htrack_w) { hthumb_w = htrack_w; } else if (hthumb_w < 30) { hthumb_w = 30; } scroll.thumb.outerWidth(hthumb_w); // Коэффициенты пропорциональности x = htrack_w - hthumb_w; scroll.ratio = (ss_sw - ss_cw) / (x < 1 ? 1 : x); if (scroll.ratio < 1) scroll.ratio = 1; // Крайние положение ползунка scroll.min_pos = 0; scroll.max_pos = htrack_w - hthumb_w; // Корректировка положения ползунка scroll.thumb.css("left", ss.scrollLeft / scroll.ratio); }
Из этого листинга хочу уделить внимание именно куску кода «Корректировка ширины contentwrap», остальное понятно и без объяснения. Смысл этого куска в следующем:
- в браузере расширеннее контента по вертикали имеет приоритет перед горизонталью — это значит, что если менять ширину окна браузера (а контент не имеет каких-либо жестких ограничений по ширине), то высота контента будет увеличиваться без ограничений, а ширина уменьшаться. По сути всё логично и дружелюбно к пользователю, т.к. наличие у окна браузера горизонтального скрола одновременно с вертикальным — это определенные неудобства, то лучше вытягивать контент по вертикали. Но когда речь идет об области прокрутки какого-нибудь элемента интерфейса, то хотелось бы, чтобы область горизонтальной прокрутки увеличивалась автоматически, когда это нужно. И вот этот код из 5ти строчек решает эту задачу: определяет, когда браузер может выстроить элементы по горизонтали и тогда производит расширение контента до нужных размеров.
Когда нужно обновлять параметры компонента?
- инициализация
- изменение размеров окна браузера
- изменение контанта
Если первые два случая весьма прозрачны, то вот последний таит в себе подводные камни. Контент может меняться не только во время удаления или добавления каких-либо элементов, но и по завершению загрузки каких-нибудь картинок, если их размеры не заданы заранее, и т.д. Можно отлавливать все варианты событий onload, но это не стоит того. Самое оптимальное решение — setInterval(update, 300) — функция update будет запускаться каждые 300 мс, нагрузки на браузер почти никакой и все весьма надежно.
Кастомизация
Кастомизация — это то, ради чего всё это и затевалось. Целью было: свести к минимуму усилия верстальщика и сэкономить время. После нескольких экспериментов со структурой элементов в скроллбаре приведенная ниже структура является более гибкой:
<div class="scrollar-scroll scrollar-vscroll"> <div class="scrollar-btn scrollar-btnup"></div> <div class="scrollar-btn scrollar-btndown"></div> <div class="scrollar-track scrollar-vtrack"> <div class="scrollar-thumb scrollar-vthumb"></div> </div> </div>
Эта верстка вертикального скроллбара (для горизонтального всё подобно). Преимущество такой структуры следующие:
- обобщающие классы scrollar-scroll, scrollar-btn, scrollar-track и scrollar-thumb позволяют задавать общие стили как для вертикального, так и для горизонтального скроллбаров (например, задав scrollar-btn { display: none;} можно скрыть кнопки);
- scrollar-btn и scrollar-track фиксируются относительно родительского элемента scrollar-scroll, что позволяет изменять их положение и их размеры, а также размеры scrollar-scroll, за счет браузера;
- элемент scrollar-track не требует стилизации, т.к. он предназначен для определения области движения бегунка scrollar-thumb, но при желании можно и к этому элементу применить стили;
- элементы позиционируются абсолютно (position: absolute) относительно scrollar-scroll, что позволяет размещать их относительно друг друга весьма легко;
По сути, вся кастомизация сводится к написанию стилей. В конце статьи будут приведены ссылки на примеры кастомизации, а также ссылки на файлы стилей для этих примеров.
А если горизонтальный или вертикальный скрол не нужен?
Все просто: при инициализации компонента нужно указать какой скрол не нужен (по умолчанию — оба отображаются). Управление видимостью и скролов производится с помощью добавления или удаления классов. Также добавление этих классов влияет на размеры контента (например, при hscroll: false ширина контента будет меняться автоматически за счет нативного функционала браузера)
Как менять контент?
Есть функция content(«action», content), где action — функция jQuery по управлению содержимым (text, html, append, prepend…). Также эту функцию можно вызвать без параметров, тогда она вернет объект jQuery и с ним можно работать, в этом случае параметры компонента будут обновляться каждые 300 мс. Примеры:
- content(«html», «Abcd») — заменит содержимое в области прокрутки на параграф с текстом, при этом обновление параметров компонента произойдет мгновенно, сразу по завершению функции;
- content().html(«Abcd») — эффект будет таким же, как и в предыдущем примере, но обновление контента произойдет за счет setInterval, в крайнем случае можно принудительно вызвать функцию update()
Ссылки к статье:
Customization — на этой странице можно посмотреть несколько вариантов кастомизации, а также можно опробовать реакцию элемента прокрутки на изменение размеров окна браузера.
GitHub — репозиторий, где можно найти всё, что описывалось в статье
Default style — пример произвольной стилизации
Safari style — стилизация скроллбаров в стиле Mac OS 10.8
Заключение.
Стоит ли изобретать что-то новое? Безусловно! У каждой новой идеи есть шанс повлиять на будущее (в конкретном случае — будущее юзабилити)