Многие посетители сайтов не знают о поиске по странице по нажатию Ctrl+F и ищут необходимый фрагмент глазами, просто пролистывая текст. Этот способ становится проблематичным, если на странице текста больше, чем три-четыре экрана. Для таких посетителей я решил реализовать поиск по странице с использованием jQuery.
Пример подобного поиска есть на сайте Конституции РФ, но там он работает как-то странно.

Предупреждение

Я не профессиональный программист, просьба не пинать за кривой код и возможное изобретение велосипеда.

HTML-форма

Первым делом разместим на странице HTML-код формы поиска. Форма включает два элемента — поле для ввода текста и DIV для вывода результатов поиска.
<input id="spterm" type="text" name="spterm" placeholder="Что искать?"><br />
<div id="spresult"> </div>

CSS

Задаём два стиля: первый — для выделения фрагмента, второй — для ссылки на первый фрагмент.
span.highlight {
background-color: #C6D9DB; cursor: pointer; }
span.splink {
color: #0A5794; cursor: pointer; }

Настройка поиска

 var minlen = 3; // минимальная длина слова
 var paddingtop = 30; // отступ сверху при прокрутке
 var scrollspeed = 200; // время прокрутки
 var keyint = 1000; // интервал между нажатиями клавиш

Подсветка фрагментов

Базовая функциональность — подсветка фрагментов в тексте. Делается это с помощью регулярных выражений.
function dosearch() {
  term = jQuery('#spterm').val();
  jQuery('span.highlight').each(function(){ //удаляем старую подсветку
   jQuery(this).after(jQuery(this).html()).remove();  
  });
  var t = '';
  jQuery('div.entry-content').each(function(){ // в селекторе задаем область поиска
   jQuery(this).html(jQuery(this).html().replace(new RegExp(term, 'ig'), '<span class="highlight">$&</span>')); // выделяем найденные фрагменты
   n = jQuery('span.highlight').length; // количество найденных фрагментов
   console.log('n = '+n);
   if (n==0)
    jQuery('#spresult').html('Ничего не найдено');
   else
    jQuery('#spresult').html('Результатов: '+n); 
  });
 }

jQuery('#spterm').keyup(function(){
  if (jQuery('#spterm').val()!=term) // проверяем, изменилась ли строка
   if (jQuery('#spterm').val().length>=minlen) { // проверяем длину строки
    dosearch(); // если все в порядке, приступаем к поиску
   }
   else
    jQuery('#spresult').html(' '); // если строка короткая, убираем текст из DIVа с результатом 
 });

Переход между фрагментами

Мало просто выделить фрагменты, гораздо удобнее организовать быстрый переход между ними. Под формой размещаем ссылку перехода на первый найденный фрагмент. Чтобы не занимать место стрелками, клик на каждый фрагмент ведет к следующему. Клик на последний фрагмент возвращает пользователя к форме поиска.
   if (n==0)
    jQuery('#spresult').html('Ничего не найдено');
   else
    jQuery('#spresult').html('Результатов: '+n); 
if (n>1) // если больше одного фрагмента, то добавляем переход между ними
   {
    var i = 0;
    jQuery('span.highlight').each(function(i){
     jQuery(this).attr('n', i++); // нумеруем фрагменты, более простого способа искать следующий элемент не нашел
    });
    jQuery('span.highlight').not(':last').attr({title: 'Нажмите, чтобы перейти к следующему фрагменту'}).click(function(){ // всем фрагментам, кроме последнего, добавляем подсказку
     jQuery('body,html').animate({scrollTop: jQuery('span.highlight:gt('+jQuery(this).attr('n')+'):first').offset().top-paddingtop}, scrollspeed); // переход к следующему фрагменту
    });
    jQuery('span.highlight:last').attr({title: 'Нажмите, чтобы вернуться к форме поиска'}).click(function(){
     jQuery('body,html').animate({scrollTop: jQuery('#spterm').offset().top-paddingtop}, scrollspeed); // переход к форме поиска
    });
   }

Задержка запуска поиска

Поиск в большом тексте и подсветка занимают несколько секунд, на которые страница зависает. При наборе длинного слова поиск производится после каждой введённой буквы.
 jQuery('#spterm').keyup(function(){
  var d1 = new Date();
  time_keyup = d1.getTime();
  if (jQuery('#spterm').val()!=term) // проверяем, изменилась ли строка
   if (jQuery('#spterm').val().length>=minlen) { // проверяем длину строки
    setTimeout(function(){ // ждем следующего нажатия
     var d2 = new Date();
     time_search = d2.getTime();
     if (time_search-time_keyup>=keyint) // проверяем интервал между нажатиями
      dosearch(); // если все в порядке, приступаем к поиску
    }, keyint); 
   }
   else
    jQuery('#spresult').html(' '); // если строка короткая, убираем текст из DIVа с результатом 
 });	

Бонус

Добавим возможность создавать ссылки на любой текст на странице без использования <a name="...">. Достаточно создать ссылку на страницу и добавить #текст.
 if (window.location.hash!="") // бонус
 {
  var t = window.location.hash.substr(1, 50); // вырезаем текст
  jQuery('#spterm').val(t).keyup(); // вставляем его в форму поиска
  jQuery('#spgo').click(); // переходим к первому фрагменту
 }

Весь код
jQuery(document).ready(function(){
 var minlen = 3; // минимальная длина слова
 var paddingtop = 30; // отступ сверху при прокрутке
 var scrollspeed = 200; // время прокрутки
 var keyint = 1000; // интервал между нажатиями клавиш
 var term = '';
 var n = 0;
 var time_keyup = 0;
 var time_search = 0;
 
 jQuery('body').delegate('#spgo', 'click', function(){
  jQuery('body,html').animate({scrollTop: jQuery('span.highlight:first').offset().top-paddingtop}, scrollspeed); // переход к первому фрагменту
 });
 
 function dosearch() {
  term = jQuery('#spterm').val();
  jQuery('span.highlight').each(function(){ //удаляем старую подсветку
   jQuery(this).after(jQuery(this).html()).remove();  
  });
  var t = '';
  jQuery('div#content').each(function(){ // в селекторе задаем область поиска
   jQuery(this).html(jQuery(this).html().replace(new RegExp(term, 'ig'), '<span class="highlight">$&</span>')); // выделяем найденные фрагменты
   n = jQuery('span.highlight').length; // количество найденных фрагментов
   console.log('n = '+n);
   if (n==0)
    jQuery('#spresult').html('Ничего не найдено');
   else
    jQuery('#spresult').html('Результатов: '+n+'. <span class="splink" id="spgo">Перейти</span>'); 
   if (n>1) // если больше одного фрагмента, то добавляем переход между ними
   {
    var i = 0;
    jQuery('span.highlight').each(function(i){
     jQuery(this).attr('n', i++); // нумеруем фрагменты, более простого способа искать следующий элемент не нашел
    });
    jQuery('span.highlight').not(':last').attr({title: 'Нажмите, чтобы перейти к следующему фрагменту'}).click(function(){ // всем фрагментам, кроме последнего, добавляем подсказку
     jQuery('body,html').animate({scrollTop: jQuery('span.highlight:gt('+jQuery(this).attr('n')+'):first').offset().top-paddingtop}, scrollspeed); // переход к следующему фрагменту
    });
    jQuery('span.highlight:last').attr({title: 'Нажмите, чтобы вернуться к форме поиска'}).click(function(){
     jQuery('body,html').animate({scrollTop: jQuery('#spterm').offset().top-paddingtop}, scrollspeed); // переход к форме поиска
    });
   } 
  });
 }

 jQuery('#spterm').keyup(function(){
  var d1 = new Date();
  time_keyup = d1.getTime();
  if (jQuery('#spterm').val()!=term) // проверяем, изменилась ли строка
   if (jQuery('#spterm').val().length>=minlen) { // проверяем длину строки
    setTimeout(function(){ // ждем следующего нажатия
     var d2 = new Date();
     time_search = d2.getTime();
     if (time_search-time_keyup>=keyint) // проверяем интервал между нажатиями
      dosearch(); // если все в порядке, приступаем к поиску
    }, keyint); 
   }
   else
    jQuery('#spresult').html(' '); // если строка короткая, убираем текст из DIVа с результатом 
 });	
 
 if (window.location.hash!="") // бонус
 {
  var t = window.location.hash.substr(1, 50); // вырезаем текст
  jQuery('#spterm').val(t).keyup(); // вставляем его в форму поиска
  jQuery('#spgo').click(); // переходим к первому фрагменту
 } 
});


Недостатки

На больших страницах (примерно 60 кб текста) скрипт зависает на пару минут.

Демо: http://jsfiddle.net/6c3ph7uj/2/

Спасибо за внимание, буду благодарен за замечания и идеи по улучшению работы скрипта.

Комментарии (10)


  1. Jabher
    30.04.2015 00:16
    +5

    Боже, мои глаза…

    На больших страницах (примерно 60 кб текста) скрипт зависает на пару минут. Все функции измерил таймерами — отрабатывают быстро (в пределах 300 мс в IE, 100 мс в Chrome). Если кто-то сможет подсказать причину — буду очень признателен.


    Надо смотреть профилирование, а не таймеры.
    И надо оптимизировать код, очень много что можно закэшировать, очень много что можно просто ускорить. Отказаться от jquery тоже хорошо поможет.

    А вообще я в принципе удивлен, что оно работает толком: нормальный поиск делается не так. Для клиента, например, можно взять Lunr

    Для применения и снятия выделения наиболее эффективно будет использовать ContentEditable, а не тупую замену регуляркой.

    jQuery(this).html().replace(new RegExp(term, 'ig'), '<span class="highlight">$&</span>')
    

    Вот это вообще страшно, тут при должных навыках можно даже инъекции сделать. Особенно вкупе с прямой ссылкой


    1. AlfaDogg
      30.04.2015 00:23
      +1

      Пардон. О каких инъекциях идет речь?


      1. Jabher
        30.04.2015 00:35

        извиняюсь, был напуган. Там прямое переписывание html идет.
        Вообще показалось, что XSS возможен. На деле возможно вроде только сломать верстку, начав вводить <, например.


    1. erfen Автор
      30.04.2015 11:06

      Спасибо, Lunr не видел, посмотрю.


  1. dom1n1k
    30.04.2015 11:21
    +4

    Зависон на 2 минуты при 60 килобайтах — это мрак, конечно.
    Да даже 5-10 секунд были бы совершенно недопустимы — юзер гарантированно уйдет!


  1. batya15
    30.04.2015 11:48
    +2

    Зачем?
    Я понимаю если бы решение было лучше стандартного поиска, или было что то новенькое (нестандартный подход), любой кто знает более менее js+jquery, реализует что то подобное, без особых затруднений


  1. kahi4
    30.04.2015 14:02
    +1

    Вообще не понял проблему. Стандартный поиск ищет замечательно, подсвечивает, у него есть стрелки «вверх-вниз». В чем его проблема?

    Единственное — поиск по регулярке. Но для этого есть плагины, да и нужно это раз в десять лет.


    1. ihoru
      30.04.2015 17:50
      +1

      Автор сразу описал проблему:

      Многие посетители сайтов не знают о поиске по странице по нажатию Ctrl+F и ищут необходимый фрагмент глазами, просто пролистывая текст.

      Другое дело, какой смысл в таком решении? Возможно, лучше было бы разместить заметную панельку с описанием того, как воспользоваться браузерным поиском по странице (ведь у всех он одинаково открывается), с возможностью ее закрытия. Заодно была бы польза для интернет сообщества — больше пользователей узнали бы о такой возможности…


      1. kahi4
        30.04.2015 21:21

        Мне кажется, что встраивать свой, альтернативный поиск, вместо встроенного в браузер, мягко говоря, плохой метод. Гораздо правильнее учить пользователей. А то такими темпами появится вторая кнопка «назад», «пуск» и так далее.


  1. Houston
    30.04.2015 20:48
    +6

    Для информации. Существует не стандартизованный метод «find» у window, который является интерфейсом к самому настоящему браузерному поиску. Возможно, в зависимости от необходимого набора браузеров, этого было бы вполне достаточно.

    jsfiddle.net/MRp2G/5