Статьи

Сделайте прокрутку внутренних ссылок плавной с помощью JavaScript

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

Они на одной странице или на другой? Должны ли они прокручивать больше отсюда? В чем дело?

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

Поиск внутренних ссылок

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

var allLinks = document.getElementsByTagName('a'); 

Нам нужно пройтись по этому списку и выяснить, какие из найденных нами ссылок являются внутренними. Внутренняя ссылка будет содержать символ хеша (#) и будет указывать на документ, который мы сейчас просматриваем. Полезный объект location сообщает нам об URL документа, который мы сейчас просматриваем, поэтому попробуйте это:

   for (var i=0;i<allLinks.length;i++) {  var lnk = allLinks[i];    if ((lnk.href && lnk.href.indexOf('#') != -1) &&        ( (lnk.pathname == location.pathname) ||    ('/'+lnk.pathname == location.pathname) ) &&        (lnk.search == location.search)) {           DO SOMETHING WITH THE LINK HERE    }  } 

Здесь цикл for просматривает список ссылок в документе, и мы проверяем три вещи:

  1. Ссылка содержит хэш?
    Мы проверяем это, используя свойство ссылки href и indexOf() чтобы найти расположение одной строки в другой.

  2. Ссылка совпадает с текущим местоположением?
    Ссылки (и объект местоположения) имеют атрибут pathname. Путь URL-адреса http://www.sitepoint.com/about/who/mharbottle.php в некоторых браузерах /about/who/mharbottle.php, а в других — о / who / mharbottle.php (обратите внимание на наличие или отсутствие первой косой черты). Мы должны проверить оба.

  3. Строка запроса совпадает с текущим местоположением?
    Строка запроса — это все, что появляется после? в URL; это очевидно важно, если ваш сайт управляется базой данных. JavaScript определяет атрибут поиска для местоположения и ссылок, которые содержат строку запроса.

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

Свиток, не прыгай!

Теперь мы определили внутреннюю ссылку, и мы хотим, чтобы она прокручивалась при нажатии. Для этого нам нужно прикрепить обработчик события onclick к ссылке. В давние времена, когда веб-разработчики были смелыми, многие думали (ну, я так и сделал), что обработчики событий были установлены для ссылки в HTML:

 <a href="http://www.sitepoint.com/" onclick="myEventHandler()"> 

Но это не совсем правда; вместо этого вы должны прикрепить прослушиватель событий к объекту ссылки. W3C определяет стандартный метод для этого, как и Internet Explorer; Скотт Эндрю с пользой предоставил функцию для обработки обоих:

 function ss_addEvent(elm, evType, fn, useCapture)  // addEvent and removeEvent  // cross-browser event handling for IE5+,  NS6 and Mozilla  // By Scott Andrew  {  if (elm.addEventListener){    elm.addEventListener(evType, fn, useCapture);    return true;  } else if (elm.attachEvent){    var r = elm.attachEvent("on"+evType, fn);    return r;  }  } 

Итак, в нашем цикле по ссылкам мы вызываем этот скрипт, чтобы прикрепить функцию плавной прокрутки к внутренней ссылке:

 ss_addEvent(lnk,'click',smoothScroll); 

Как прокрутить

Конечно, нам действительно smoothScroll() функция smoothScroll() . Это сложный аспект, потому что все дело в поиске позиции объекта на странице, и разные браузеры реализуют это различными способами. Чудесный Эндрю Кловер написал обзор того, как найти эту позицию в разных браузерах, и мы будем широко использовать это решение здесь.

Во-первых, наша функция smoothScroll является обработчиком событий, поэтому, когда она вызывается (т.е. когда пользователь нажимает одну из наших внутренних ссылок), нам нужно получить ссылку, по которой щелкнули. Браузеры класса Netscape передают объект события каждому обработчику; Internet Explorer сохраняет эти данные в глобальном объекте window.event .

   if (window.event) {    target = window.event.srcElement;  } else if (e) {    target = e.target;  } else return; 

Этот код устанавливает нажатие ссылки в качестве цели кросс-браузерным способом. … Ну, почти. Mozilla иногда передает вам текстовый узел в ссылке как нажатый элемент. Нам нужно проверить, является ли target текстовым узлом (то есть равен ли его nodeType 3), и взять его родителя, если он есть.

 if (target.nodeType == 3) { target = target.parentNode; } 

Просто чтобы быть параноиком, мы также проверяем, что у нас есть тег A, на случай, если мы что-то пропустили:

 if (target.nodeName.toLowerCase() != 'a') return; 

Теперь нам нужно найти пункт назначения: тег <a name> который соответствует части после хеша в нашей нажатой ссылке. У ссылок есть атрибут hash, который содержит # и раздел, который появляется после него в URL-адресе, поэтому давайте теперь пройдемся по всем ссылкам в документе и проверим, равен ли их атрибут name хеш-части щелкнувшей ссылки:

   // First strip off the hash (first character)  anchor = target.hash.substr(1);  // Now loop all A tags until we find one with that name  var allLinks = document.getElementsByTagName('a');  var destinationLink = null;  for (var i=0;i<allLinks.length;i++) {    var lnk = allLinks[i];    if (lnk.name && (lnk.name == anchor)) {      destinationLink = lnk;      break;    }  }  // If we didn't find a destination, give up and let the browser do  // its thing  if (!destinationLink) return true; 

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

   var destx = destinationLink.offsetLeft;  var desty = destinationLink.offsetTop;  var thisNode = destinationLink;  while (thisNode.offsetParent &&        (thisNode.offsetParent != document.body)) {    thisNode = thisNode.offsetParent;    destx += thisNode.offsetLeft;    desty += thisNode.offsetTop;  } 

Обратите внимание, что мы перебираем offsetParents пока не доберемся до тела документа, как того требует IE. Затем выясните, где мы сейчас находимся:

 function ss_getCurrentYPos() {  if (document.body && document.body.scrollTop)    return document.body.scrollTop;  if (document.documentElement && document.documentElement.scrollTop)    return document.documentElement.scrollTop;  if (window.pageYOffset)    return window.pageYOffset;  return 0;  } 

IE5 и 5.5 хранят текущую позицию в document.body.scrollTop , IE6 в document.documentElement.scrollTop и браузеры класса Netscape в window.pageYOffset . Уф!

То, как мы на самом деле обрабатываем прокрутку, это использование setInterval() ; эта очень полезная функция устанавливает повторяющийся таймер, который запускает функцию по нашему выбору. В этом случае наша функция будет перемещать позицию браузера на один шаг ближе к месту назначения; setInterval() будет неоднократно вызывать нашу функцию, и когда мы достигнем места назначения, мы отменим таймер.

Во-первых, используйте clearInterval() чтобы отключить все таймеры, которые в данный момент работают:

   clearInterval(ss_INTERVAL); 
  ss_INTERVAL - это глобальная переменная, в которой мы позже будем хранить выходные данные setInterval() .  Затем выясните, насколько большим должен быть каждый шаг:

   ss_stepsize = parseInt((desty-cypos)/ss_STEPS); 
  ss_STEPS определено в скрипте как количество шагов, которые мы делаем от цели до цели.  Наша функция «прокрутить на один шаг» называется ss_scrollWindow и принимает три параметра: 

  • сколько прокручивать
  • место назначения
  • сама ссылка назначения

Нам нужно сконструировать вызов this в строке и передать эту строку в setInterval вместе с частотой, с которой мы хотим, чтобы вызов повторялся:

   ss_INTERVAL = setInterval('ss_scrollWindow('+ss_stepsize+','+desty+',"'+anchor+'")',10); 

Обратите внимание, как мы строим строку, которая является вызовом ss_scrollWindow() , а не просто вызываем ss_scrollWindow() напрямую — это одна из самых запутанных вещей в setInterval() .

Как только мы это сделаем, мы должны остановить браузер, взяв его нормальный курс, подчиняясь ссылке и переходя непосредственно к месту назначения. Опять же, это происходит по-разному в разных браузерах. Чтобы остановить браузер, обрабатывающий это событие нормально в Internet Explorer, используйте:

   if (window.event) {    window.event.cancelBubble = true;    window.event.returnValue = false;  } 

Обратите внимание на проверку для window.event чтобы убедиться, что мы используем IE.

Чтобы сделать то же самое в браузерах класса Netscape, используйте этот код:

   if (e && e.preventDefault && e.stopPropagation) {    e.preventDefault();    e.stopPropagation();  } 
Прокрутка шага

И последнее: как мы на самом деле делаем прокрутку? Ключевой функцией здесь является window.scrollTo() , которой вы передаете позиции X и Y; затем браузер прокручивает окно до этой позиции. Одна небольшая складка в том, что вы не можете прокрутить весь путь до самого дна. Если позиция Y, по которой вы проходите, меньше высоты окна от нижней части документа, браузер будет прокручиваться вниз только настолько, насколько это возможно — очевидно, он не сможет перейти прямо вниз по ссылке, если расстояние до нижняя часть страницы меньше высоты окна.

Теперь нам нужно проверить это; лучший способ сделать это — увидеть, одинаковы ли позиции до и после прокрутки:

 function ss_scrollWindow(scramount,dest,anchor) {  wascypos = ss_getCurrentYPos();  isAbove = (wascypos < dest);  window.scrollTo(0,wascypos + scramount);  iscypos = ss_getCurrentYPos();  isAboveNow = (iscypos < dest);  if ((isAbove != isAboveNow) || (wascypos == iscypos)) {    // if we've just scrolled past the destination, or    // we haven't moved from the last scroll (ie, we're at the    // bottom of the page) then scroll exactly to the link    window.scrollTo(0,dest);    // cancel the repeating timer    clearInterval(ss_INTERVAL);    // and jump to the link directly so the URL's right    location.hash = anchor;  }  } 

Обратите внимание, что, поскольку мы прокручиваем определенные целочисленные приращения, этот шаг мог пройти мимо цели. Таким образом, мы проверяем, были ли мы над ссылкой до и после прокрутки; если эти два местоположения различны, мы прокручиваем мимо ссылки, и поэтому мы закончили. Если мы закончили, мы отменили таймер и установили URL страницы (установив немного объекта location), чтобы он выглядел так, как будто браузер обработал ссылку.

Создание эффекта произойдет

Самый простой способ применить этот эффект к вашим страницам — поместить код в файл с именем smoothscroll.js и включить этот файл на свою страницу, используя этот код:

 <script src="smoothscroll.js" type="text/javascript"></script> 

Этот подход следует принципам ненавязчивого DHTML, что делает его простым для всех. Чтобы решение работало, скрипт должен быть запущен чем-то; мы помещаем код из нашего первого шага (перебирая ссылки, чтобы найти внутренние) в функцию ss_fixAllLinks() и привязываем его к событию onload окна, используя функцию Скотта Эндрю:

 ss_addEvent(window,"load",ss_fixAllLinks); 

Весь код выглядит так:

 function ss_fixAllLinks() {  // Get a list of all links in the page  var allLinks = document.getElementsByTagName('a');  // Walk through the list  for (var i=0;i<allLinks.length;i++) {    var lnk = allLinks[i];    if ((lnk.href && lnk.href.indexOf('#') != -1) &&        ( (lnk.pathname == location.pathname) ||    ('/'+lnk.pathname == location.pathname) ) &&        (lnk.search == location.search)) {      // If the link is internal to the page (begins in #)      // then attach the smoothScroll function as an onclick      // event handler      ss_addEvent(lnk,'click',smoothScroll);    }  }  }   function smoothScroll(e) {  // This is an event handler; get the clicked on element,  // in a cross-browser fashion  if (window.event) {    target = window.event.srcElement;  } else if (e) {    target = e.target;  } else return;   // Make sure that the target is an element, not a text node  // within an element  if (target.nodeType == 3) {    target = target.parentNode;  }   // Paranoia; check this is an A tag  if (target.nodeName.toLowerCase() != 'a') return;   // Find the <a name> tag corresponding to this href  // First strip off the hash (first character)  anchor = target.hash.substr(1);  // Now loop all A tags until we find one with that name  var allLinks = document.getElementsByTagName('a');  var destinationLink = null;  for (var i=0;i<allLinks.length;i++) {    var lnk = allLinks[i];    if (lnk.name && (lnk.name == anchor)) {      destinationLink = lnk;      break;    }  }   // If we didn't find a destination, give up and let the browser do  // its thing  if (!destinationLink) return true;   // Find the destination's position  var destx = destinationLink.offsetLeft;  var desty = destinationLink.offsetTop;  var thisNode = destinationLink;  while (thisNode.offsetParent &&        (thisNode.offsetParent != document.body)) {    thisNode = thisNode.offsetParent;    destx += thisNode.offsetLeft;    desty += thisNode.offsetTop;  }   // Stop any current scrolling  clearInterval(ss_INTERVAL);   cypos = ss_getCurrentYPos();   ss_stepsize = parseInt((desty-cypos)/ss_STEPS);  ss_INTERVAL = setInterval('ss_scrollWindow('+ss_stepsize+','+desty+',"'+anchor+'")',10);   // And stop the actual click happening  if (window.event) {    window.event.cancelBubble = true;    window.event.returnValue = false;  }  if (e && e.preventDefault && e.stopPropagation) {    e.preventDefault();    e.stopPropagation();  }  }   function ss_scrollWindow(scramount,dest,anchor) {  wascypos = ss_getCurrentYPos();  isAbove = (wascypos < dest);  window.scrollTo(0,wascypos + scramount);  iscypos = ss_getCurrentYPos();  isAboveNow = (iscypos < dest);  if ((isAbove != isAboveNow) || (wascypos == iscypos)) {    // if we've just scrolled past the destination, or    // we haven't moved from the last scroll (ie, we're at the    // bottom of the page) then scroll exactly to the link    window.scrollTo(0,dest);    // cancel the repeating timer    clearInterval(ss_INTERVAL);    // and jump to the link directly so the URL's right    location.hash = anchor;  }  }   function ss_getCurrentYPos() {  if (document.body && document.body.scrollTop)    return document.body.scrollTop;  if (document.documentElement && document.documentElement.scrollTop)    return document.documentElement.scrollTop;  if (window.pageYOffset)    return window.pageYOffset;  return 0;  }   function ss_addEvent(elm, evType, fn, useCapture)  // addEvent and removeEvent  // cross-browser event handling for IE5+,  NS6 and Mozilla  // By Scott Andrew  {  if (elm.addEventListener){    elm.addEventListener(evType, fn, useCapture);    return true;  } else if (elm.attachEvent){    var r = elm.attachEvent("on"+evType, fn);    return r;  }  }   var ss_INTERVAL;  var ss_STEPS = 25;   ss_addEvent(window,"load",ss_fixAllLinks); 
Завершение

Внутренние ссылки вашего документа будут прокручиваться к месту назначения, что позволит вашим пользователям сохранять информацию о том, где находится браузер в документе и как далеко они находятся от начальной точки. Код был протестирован и работает в Mozilla, IE и Opera; он не работает в Konqueror и, как предполагается, не работает в других браузерах.