Статьи

Построение коробки, которая прилипает при прокрутке

Липкие поля — это поля, которые остаются видимыми в вашем браузере, независимо от того, где вы прокручиваете страницу. Они чаще всего используются на боковых панелях и панелях заголовков, чтобы брендинг и навигационные меню были всегда видны и доступны. В старые времена склеенные ящики были довольно простыми и стояли только на одной части области просмотра независимо от того, где вы прокручивали, как показано на этом скриншоте Yahoo! домашняя страница.

yahoo_2001

И их было довольно легко реализовать с помощью CSS, как показано в следующем обходном пути IE6.

<style> #header { position: fixed; top: 0px; } * html #header { position: absolute; top: expression(document.body.scrollTop); } </style> 

Но в настоящее время веб-страницы развиваются, и склеенные ящики должны находиться в разных местах в зависимости от того, куда была прокручена веб-страница. Например, посмотрите демонстрационную страницу этой статьи, магазины йоги по всему миру . Обратите внимание, как логотип и речевые пузыри изящно плавают рядом с основным содержанием. Когда вы находитесь в верхней части страницы, липкая рамка может быть закреплена в середине экрана. Когда вы прокручиваете страницу вниз, стикер изящно скользит вверх, а затем цепляется за верхнюю часть окна просмотра на время сеанса. Затем, когда вы приблизитесь к нижней части страницы (или к границе), стикер будет скользить вверх, пока не исчезнет из поля зрения. Это очень плавный опыт, который можно сделать всего несколькими строками кода.

План

Во-первых, нам понадобится функция, которая вызывается при прокрутке страницы. Эта функция должна пройти через каждый из прикрепленных элементов на странице, чтобы определить, является ли элемент:

  1. Ниже верхнего края окна просмотра.
  2. Над верхним краем области просмотра и
    • Не касаясь нижнего края его границы.
    • Касаясь нижнего края его границы.

Теперь давайте запустим следующий скелетный код:

 document.onscroll = onScroll; function onScroll() { var list = getAllStickies(); for (var i = 0, item; item = list[i]; i++) { var bound = getBoundary(item); var edge = bound.getBoundingClientRect().bottom; var height = item.offsetHeight; var top = item.getBoundingClientRect().top; if (top < 0) { // above the top edge of the viewport if (edge > height) { // not touching the bottom edge of its boundary item.style.position = "fixed"; item.style.top = "0px"; } else { // touching the bottom edge of its boundary item.style.position = "relative"; item.style.top = -((top - edge) + height) + "px"; } } else { // below the top edge of the viewport item.style.position = "relative"; item.style.top = "auto"; } } } 

Функции getAllStickies() и getBoundary() еще не определены. Мы вернемся к ним чуть позже. Функция getBoundingClientRect() — это удобная и быстрая функция для возврата позиции элемента относительно области просмотра. Элементы над окном просмотра являются отрицательными числами. Используя эту функцию, нам нужно только проверить, является ли верхнее значение положительным или отрицательным числом.

Наша функция обнаруживает три сценария для каждого липкого элемента:

  1. Если элемент находится ниже верхнего края области просмотра, элемент по-прежнему является частью страницы и должен находиться в своем естественном положении, чтобы он прокручивался вместе со страницей.
  2. Если элемент находится над верхним краем области просмотра (т. Е. Скрыт) и не касается нижнего края его границы, элемент следует переместить в верхнюю часть области просмотра, и его position должно быть fixed .
  3. Если элемент находится над верхним краем области просмотра (т. Е. Скрыт) и касается нижнего края его границы, элемент следует переместить так, чтобы он находился чуть выше края границы. В этом случае его position устанавливается relative чтобы он мог прокручивать страницу.

Теперь, когда логика на месте, давайте обсудим семантику.

Отметка

Мы определим липкий элемент как элемент, содержащий атрибут x-sticky . Sticky — это потомок или потомок граничного элемента, идентифицируемого атрибутом x-sticky-boundary . Липкий свободно перемещается в пределах граничного элемента. Пример закрепления и границы показан ниже.

 <div x-sticky-boundary=""> <div x-sticky="">I am a sticky confined within a boundary</div> </div> 

Далее мы реализуем функции getAllStickies() и getBoundary() мы упоминали ранее. Мы можем заменить getAllStickies() на:

 var list = document.querySelectorAll("[x-sticky]"); 

Кроме того, мы можем реализовать getBoundary() чтобы вернуть первый элемент-предок с атрибутом x-sticky-boundary getBoundary() (или вернуть элемент body ):

 function getBoundary(n) { while (n = n.parentNode) { if (n.hasAttribute("x-sticky-boundary")) { return n; } } return document.body || document.documentElement; } 

В настоящее время код поддерживает только одну наклейку на границу. Но часто у нас есть две или более наклеек на границе, которые не должны конфликтовать друг с другом Если второй стикер перемещается в верхнюю часть области просмотра, он должен оттолкнуть первый блок в сторону.

Ранее мы предполагали, что нижний край границы является граничным пределом. Нам нужно изменить это, чтобы также проверить верхний край следующего липкого элемента, который находится в той же границе.

 var edge = bound.getBoundingClientRect().bottom; var nextItem = findNextInBoundary(list, i, bound); if (nextItem) { edge = nextItem.getBoundingClientRect().top; } 

Мы определили новую функцию findNextInBoundary() , которая перебирает массив, начиная с определенного индекса, в поисках следующей липкой, которая разделяет граничный элемент с текущей липкой.

Падение

Есть один важный сценарий, который мы до сих пор не рассмотрели. После прокрутки страницы мы динамически переместили липкий элемент в другую позицию на странице. Это означает, что исходное положение липкого элемента не сохраняется, а это означает, что мы не можем восстановить его исходное положение, когда пользователь прокручивает назад вверх.

Кроме того, когда мы делаем элемент закрепления фиксированным, он вытягивается из потока документов, что означает, что содержимое под ним будет смещаться вверх. Мы хотим сохранить занимаемое им пространство, чтобы содержимое под ним не перепрыгивало. Чтобы обойти это, нам нужно поместить элемент-заполнитель в исходное положение стикера. Мы также поместим наклейку внутри заполнителя, чтобы он не влиял на псевдоселектор nth-child для родственных элементов. Затем всякий раз, когда нам нужно восстановить позицию залипания, мы заменяем заполнитель на залипание и отбрасываем заполнитель.

Следует помнить одну вещь: если мы хотим получить начальную позицию стикера, мы должны вместо этого получить текущую позицию его заполнителя. Вот наша обновленная функция:

 document.onscroll = onScroll; function onScroll() { var list = document.querySelectorAll("[x-sticky]"); for (var i = 0, item; item = list[i]; i++) { var bound = getBoundary(item); var edge = bound.getBoundingClientRect().bottom; var nextItem = findNextInBoundary(list, i, bound); if (nextItem) { if(nextItem.parentNode.hasAttribute("x-sticky-placeholder")) { nextItem = nextItem.parentNode; } edge = nextItem.getBoundingClientRect().top; } // check if the current sticky is already inside a placeholder var hasHolder = item.parentNode.hasAttribute("x-sticky-placeholder"); var rect = item.getBoundingClientRect(); var height = rect.bottom - rect.top; // get the height and width var width = rect.right - rect.left; var top = hasHolder ? item.parentNode.getBoundingClientRect().top : rect.top; if (top < 0) { if(edge > height) { item.style.position = "fixed"; item.style.top = "0px"; } else { item.style.position = "relative"; item.style.top = -((top - edge) + height) + "px"; } if (!hasHolder) { //create the placeholder var d = document.createElement("div"); d.setAttribute("x-sticky-placeholder", ""); d.style.height = height + "px"; //set the height and width d.style.width = width + "px"; item.parentNode.insertBefore(d, item); d.appendChild(item); } } else { item.style.position = "relative"; item.style.top = "auto"; if (hasHolder) { //remove the placeholder item = item.parentNode; item.parentNode.insertBefore(item.firstChild, item); item.parentNode.removeChild(item); } } } } function findNextInBoundary(arr, i, boundary) { i++; for (var item; item = arr[i]; i++) { if (getBoundary(item) == boundary) { return item; } } } function getBoundary(n) { while (n = n.parentNode) { if (n.hasAttribute("x-sticky-boundary")) { return n; } } return document.body || document.documentElement; } 

Приманка

Чтобы максимизировать полезность заполнителя, нам также необходимо скопировать несколько свойств CSS из закрепленного элемента в заполнитель. Например, мы хотим, чтобы поля были одинаковыми, чтобы они занимали одинаковое количество места. Мы также хотим, чтобы свойство float было сохранено, чтобы оно не испортило плавающие макеты сетки.

Давайте введем функцию copyLayoutStyles() , которая вызывается, как только создается заполнитель, для копирования стилей в заполнитель:

 function copyLayoutStyles(to, from) { var props = { marginTop: 1, marginRight: 1, marginBottom: 1, marginLeft: 1 }; if (from.currentStyle) { props.styleFloat = 1; for (var s in props) { to.style[s] = from.currentStyle[s]; } } else { props.cssFloat = 1; for (var s in props) { to.style[s] = getComputedStyle(from, null)[s]; } } } 

Очистка

В настоящее время мы устанавливаем свойство position элемента непосредственно на fixed или relative . Давайте переместим этот вызов в таблицу стилей CSS и используем селекторы для применения свойства. Это позволяет другим программистам при необходимости переопределять поведение по умолчанию. Таблица стилей CSS будет выглядеть так:

 <style> [x-sticky] {margin:0} [x-sticky-placeholder] {padding:0; margin:0; border:0} [x-sticky-placeholder] > [x-sticky] {position:relative; margin:0 !important} [x-sticky-placeholder] > [x-sticky-active] {position:fixed} </style> 

Вместо того, чтобы создавать отдельную таблицу стилей, давайте внедрим эту таблицу стилей, используя JavaScript, создав временный элемент и установив его innerHTML вместе с таблицей стилей. Затем мы можем добавить результат в документ, как показано ниже.

 var css = document.createElement("div"); css.innerHTML = ".<style>" + "[x-sticky] {margin:0}" + "[x-sticky-placeholder] {padding:0; margin:0; border:0}" + "[x-sticky-placeholder] > [x-sticky] {position:relative; margin:0 !important}" + "[x-sticky-placeholder] > [x-sticky-active] {position:fixed}" + "<\/style>"; var s = document.querySelector("script"); s.parentNode.insertBefore(css.childNodes[1], s); 

Внутри основной функции нам нужно заменить каждое item.style.position = "fixed" , item.style.position = "fixed" , на item.setAttribute("x-sticky-active", "") , чтобы селектор CSS мог соответствовать атрибуту. Для того, чтобы этот код можно было пересылать, нам также нужно заключить все в замыкание, чтобы частные переменные были приватными. Нам также нужно будет использовать addEventListener() вместо назначения для document.onscroll чтобы избежать возможных конфликтов. И, пока мы это делаем, давайте добавим проверку API (показанную ниже), чтобы наша функция не работала в старых браузерах.

 if (document.querySelectorAll && document.createElement("b").getBoundingClientRect) (function(doc) { "use strict"; init(); function init() { if(window.addEventListener) { addEventListener("scroll", onScroll, false); } else { attachEvent("onscroll", onScroll); } var css = doc.createElement("div"); css.innerHTML = ".<style>" + "[x-sticky] {margin:0}" + "[x-sticky-placeholder] {padding:0; margin:0; border:0}" + "[x-sticky-placeholder] > [x-sticky] {position:relative; margin:0!important}" + "[x-sticky-placeholder] > [x-sticky-active] {position:fixed}<\/style>"; var s = doc.querySelector("script"); s.parentNode.insertBefore(css.childNodes[1], s); } function onScroll() { var list = doc.querySelectorAll("[x-sticky]"); for (var i = 0, item; item = list[i]; i++) { var bound = getBoundary(item); var edge = bound.getBoundingClientRect().bottom; var nextItem = findNextInBoundary(list, i, bound); if (nextItem) { if (nextItem.parentNode.hasAttribute("x-sticky-placeholder")) { nextItem = nextItem.parentNode; } edge = nextItem.getBoundingClientRect().top; } var hasHolder = item.parentNode.hasAttribute("x-sticky-placeholder"); var rect = item.getBoundingClientRect(); var height = rect.bottom - rect.top; var width = rect.right - rect.left; var top = hasHolder ? item.parentNode.getBoundingClientRect().top : rect.top; if (top < 0) { if (edge > height) { if (!item.hasAttribute("x-sticky-active")) { item.setAttribute("x-sticky-active", ""); } item.style.top = "0px"; } else { if (item.hasAttribute("x-sticky-active")) { item.removeAttribute("x-sticky-active"); } item.style.top = -((top - edge) + height) + "px"; } if (!hasHolder) { var d = doc.createElement("div"); d.setAttribute("x-sticky-placeholder", ""); d.style.height = height + "px"; d.style.width = width + "px"; copyLayoutStyles(d, item); item.parentNode.insertBefore(d, item); d.appendChild(item); } } else { if (item.hasAttribute("x-sticky-active")) { item.removeAttribute("x-sticky-active"); } item.style.top = "auto"; if(hasHolder) { item = item.parentNode; item.parentNode.insertBefore(item.firstChild, item); item.parentNode.removeChild(item); } } } } function findNextInBoundary(arr, i, boundary) { i++; for (var item; item = arr[i]; i++) { if (getBoundary(item) == boundary) { return item; } } } function getBoundary(n) { while (n = n.parentNode) { if (n.hasAttribute("x-sticky-boundary")) { return n; } } return doc.body || doc.documentElement; } function copyLayoutStyles(to, from) { var props = { marginTop: 1, marginRight: 1, marginBottom: 1, marginLeft: 1 }; if (from.currentStyle) { props.styleFloat = 1; for (var s in props) { to.style[s] = from.currentStyle[s]; } } else { props.cssFloat = 1; for (var s in props) { to.style[s] = getComputedStyle(from, null)[s]; } } } })(document); 

Вывод

И вот оно! Помечая элемент атрибутом x-sticky , он прокручивает страницу до тех пор, пока не достигнет вершины, и будет задерживаться до тех пор, пока не достигнет граничного края, где затем исчезнет вверх по странице.