Ajax-автозаполнение (Ajax-autosuggest) своими силами

Date January 15th, 2010 Author Vitaly Agapov

ajaxТех, кто хоть иногда заходит в Интернет, автозаполнением поисковых форм уже никак не удивишь. Даже более того – эта фича становится де-факто обязятельной для реализации в любом web-проекте с функцией поиска. Впервые функциональность автозаполнения была реализована в Google Suggest, и с тех пор распространилась повсеместно. Действительно очень удобно – вводишь несколько первых букв в текстовое поле, а тебе уже выдают выпадающий список с возможными поисковыми строками, содержащими введённый тобой текст. Круто. Особенно крутость этого механизма ощущается после того, как узнаёшь, насколько просто её можно реализовать самому. И речь даже не о десятках (сотнях?) платных и бесплатных JS-библиотек, встречающихся там и тут на просторах всемирного “невода”, а о том, как можно её сделать самому с нуля своими руками. Об этом ниже…

Тут я, правда, делаю себе поблажку, так как использую для этих целей библиотеку jQuery. С ней всё получается ещё проще. Нет-нет, можно и без неё. Я даже знаю как… Но это в другой раз. Может быть.

Итак, делаем для начала форму поиска – текстовое поле с кнопкой. Заодно подключим jQuery и сделаем div для последующего вывода результатов автозаполнения.

<script src="/js/jquery.min.js" type="text/javascript" language="Javascript"></script>

<form id="searchform" action='/search'>
<input class="text_input"  autocomplete='off' name="q" onkeydown="keydown(this, event)"  onkeyup="javascript:search(this,event)"/>
<input  type="submit" value="Поиск"/>
</form>

<div class="resultdropdown" id="result" style="position:absolute"></div>

Само собой, всё оформление и CSS-стили оставим за кадром. Тут уж кто во что горазд. Стоит ещё заметить, что div для результатов можно изначально вставить в любое место страницы.

Теперь надо определить функции search и keydown, которые будут обрабатывать нажатия клавиш, когда фокус находится в нашем поле ввода. Этих функций потребовалось две по простой причине – обработчик нажатия на Enter нельзя засунуть в onkeyup, так как при этом срабатывание события onsubmit формы будет происходить раньше, а поиск в onkeydown лучше не помещать, так как слегка нарушится эргономика. В общем, откроем наш js и напишем туда что-то вроде этого:

var suggest_count = 0;
var searchq = "";
function select_popup(num) {
     num--;
     $("#result table tr").removeClass("active");
     $("#result table tr").eq(num).addClass("active");
}
var popup_counter=0;
function keydown(el,evt) {
     if (!evt) var evt = window.event;
     var key = evt.keyCode || evt.which;
     if (key==13) // Enter
     {
          if (popup_counter<1) { document.getElementById("searchform").onsubmit=function() { return true }; }
          window.location=$("#result table tr").eq(popup_counter-1).find("td").find("a").attr("href");
     }
     else if (key == 27) // esc
     {
          $(".resultdropdown").html("");
          popup_counter=0;
     }
}
function search(el, evt) {
     if (!evt) var evt = window.event;
     var key = evt.keyCode || evt.which;
     if (key==0 || key==8 || ( key > 45 && key < 112) || (key > 123))
     {
        suggest_count++;
        var offset = $(el).offset();
        var top = offset.top+15;
        var left = offset.left;
        searchq = $(el).val();
        setTimeout("searchGo ("+top+","+left+","+suggest_count+")",300);
      }
      else if (key==40 && popup_counter<$("#result table tr").size()) // Down
      {
         document.getElementById("searchform").onsubmit=function() {return false};
         popup_counter++;
         select_popup(popup_counter);
      }
      else if (key==38 && popup_counter>0) // Up
      {
          popup_counter--;
          select_popup(popup_counter);
      }
}

function searchGo(top, left, count) {
      if (count == suggest_count)
     {
           var window = $("#result");
           window.css('left', left).css('top', top).css('z-index', '10005');

           $.ajax({
                url: '/search?q='+searchq,
                cache: false,
                success: function(html) {
                     window.html(html);
                }
           });
      }
}

Здесь мы перевыполнили план – вместо одной функции сделали целых несколько. Первая функция – search – ведёт счетчик вызовов самой себя (то есть срабатываний событий onkeyup на текстовом поле), определяет координаты для вывода окошка со списком и вызывает в асинхронном режиме с задержкой 300мс вторую функцию – searchGo, которая уже и производит Ajax-обращение на сервер. Но делает она это хитро. Прежде, чем слать запрос на сервер, она проверяет, а не изменился ли счетчик, за эти самые последние 300мс, то есть сравнивает текущее значение счетчика с тем значением, которое ей было передано в качестве параметра функцией search. Если счетчик изменился, то функция searchGo ничего не делает. Нужно это для того, чтобы не загружать сервер потоком GET-запросов, если пользователь быстро набирает слово для поиска. Обращение к серверу произойдёт только при небольшой паузе… Ну, задумался пользователь слегка, а мы тут ему р-раз – и всё варианты на блюдечке.

Остальные функции предназначены для возможности выбора выпадающих вариантов с помощью стрелок на клавиатуре. При нажатии на клавиши вниз или вверх (коды 40 и 38 соответственно) функция search изменяет переменную popup_counter, в которой постоянно хранится номер строки в выпадающем списке, которая сейчас выделена. Функция select_popup занимается подсвечиванием нужной строки. В данном случае она присваивает элементу tr класс active (об определении стиля этого класса надо еще поработать с css). А функция keydown отслеживает нажатия на Esc и Enter. С Esc всё просто – мы убираем содержимое выпадающего списка и обнуляем переменную popup_counter. При нажатии на Enter мы проверяем, не выделена ли сейчас какя-либо опция. Если ничего не выделено (popup_counter нулевой), то мы включаем обработку события onsubmit формы, чтобы введенный текст в штатном режиме в запросе POST полетел на сервер. В противном случае мы заставляем браузер загрузить страницу, указанную в атрибуте href ссылки внутри выделенной строки таблицы. Обработка события onsubmit, кстати, выключается при нажатии клавиши "Вниз"

Естественно, для функционирования нам еще понадобится на сервере приложение, которое непосредственно будет производить поиск. Будет ли оно лезть в базу данных, в файл или просто смотреть в свои статические списки – не важно. Важно, чтобы это приложение выводило данные в том виде, в котором мы хотим их видеть во всплывающем/выпадающем списке. Я для примера назвал его search, и оно принимает всего один аргумент q с текстом запроса.

В таком виде всё это должно уже нормально работать. Можно приложить голову и сделать хорошее оформление, быстрый оптимизированный поиск… Но это потом. Сейчас же для полного счастья не хватает одной маленькой неочевидной вещи – надо, чтобы наше всплывающее окно с автозаполнением как-то закрывалось. Например, если кликнуть мышью в любую область экрана, кроме самого всплывающего окна, как, в общем-то, нам подсказывает здравый смысл и весь опыт общения с пользовательскими интерфейсами.

Для этого (о, jQuery!) делаем это:

        <script type="text/javascript" language="Javascript">
        //<![CDATA[
            $(document).ready(function(){
                $("*:not(.resultdropdown)").click(function() {
                        $(".resultdropdown").html("");
                });
            });
        //]]>
        </script>

Этот код можно вставить в основную страницу, и наше желание осуществится. Впрочем, можно не извращаться и воткнуть js-код в js-файл.

На этом – всё.

Tags: ,
Category: Web-dev | 14 Comments »

Comments

14 комментариев на “Ajax-автозаполнение (Ajax-autosuggest) своими силами”

  1. golddimas

    Хотелось бы рабочий пример, с кодом серверной части, который будет производить поиск в базе данных.

  2. Vitaly Agapov

    Код серверной части – это отдельная история. Это может быть PHP или Perl-скрипт. Это даже может быть статичная HTML-страница. Писать об этом неинтересно.
    Есть конкретные вопросы? Что-то не работает?

  3. andery

    а как сделать автозаполнение с картинками (т.е. картинка и возле неё совпадения слов)

  4. Vitaly Agapov

    Пока что не очень понятно, что именно требуется. Если имеется в виду то, что в выпадающем списке должен быть не только текст, но и какие-то картинки, то никаких ограничений здесь нет.
    То, что выводится в выпадающий список – это обычный кусок HTML-разметки, возвращаемый скриптом search (в моём случае), так что туда можно напихать и тэги , и тэги и вообще что угодно. Будут и картинки, будут и видеоролики.

  5. Scorpion

    Мне бы хотелось узнать, как можно сделать так, что бы при нажатии на поле, выпадал весь список из БД?????

  6. Vitaly Agapov

    Чтобы список выпадал сразу при нажатии на поле, надо вызывать функцию по событию onfocus. Функция аяксом обращается к скрипту, а скрипт генерит кусок html-кода с табличкой, заполняя её тем, что ему вернет SELECT * FROM …

  7. Владимир

    Вопрос
    //

    Эта хрень чето не срабатывает. + не работает листинг помогите сделать…

  8. Vitaly Agapov

    С удовольствием помогу. Киньте ссылку на страницу, где всё это не работает. Либо перешлите все исходники, я посмотрю.

  9. web-kol

    Любопытно, спасибо! Можно поковырять. Давно ищу решение, чтобы можно было удалять выбранные варианты крестиком…

  10. Rau

    Желаю автору, что бы все что он хотел бы узнать и получить – он получал в таком же виде как эта статья, то есть без “серверной части”. Торт без крема, машину без коробки передач, книги без четных страниц и тд…
    Это будет справедливо.

    ps
    я только начал изучать js, у меня на данном этапе нет шанса понять самому как подцепить сервер. :(

  11. Роман

    Мне кажется что счётчик нажатий клавиш вверх вниз не работает почему то

  12. Андрей

    Спасибо большое! Мне необходимо было сделать свой autosuggest в качестве небольшого задания, за основу я взял ваш код. Исходный вариант у меня немного не работал, поэтому я слегка его переделал, кое-где что-то убрал, что-то добавил, но в общем и целом исходник не претерпел сильных изменений. Были вопросы в комментариях на счет серверной стороны, я пожалуй её тоже выложу, но надо учесть,что я делал под ASP.Net MVC, хотя принцип один и тот же. Рад буду – если кому-то поможет.
    итак, скрипт:

    var searchq = “”;
    var popup_counter = 0;

    function select_popup(num) {
    if (num > 0) {
    num–;
    $(“#result table tr”).removeClass(“active”);
    $(“#result table tr”).eq(num).addClass(“active”);
    }
    else {
    $(“#result table tr”).removeClass(“active”);
    $(“#suggestInput”).focus();
    }
    }

    function keydown(el, evt) {
    if (!evt) var evt = window.event;
    var key = evt.keyCode || evt.which;
    if (key == 13 && popup_counter > 0) // Enter
    {
    window.location = $(“#result table tr”).eq(popup_counter – 1).find(“td”).find(“a”).attr(“href”);
    }
    else if (key == 27) // esc
    {
    $(“.resultdropdown”).html(“”);
    popup_counter = 0;
    }
    }

    function search(el, evt) {
    if (!evt) var evt = window.event;
    var key = evt.keyCode || evt.which;
    if (key == 8 || key == 32 || (key > 48 && key 188)) //backspace, space,0-9,a-z, запятые кавычки и проч.
    {
    searchq = $(el).val();
    setTimeout(searchGo(), 200);
    }
    else if (key == 40 && popup_counter 0) // Up
    {
    popup_counter–;
    select_popup(popup_counter);
    }
    }

    function searchGo() {
    var resultDiv = $(“#result”);
    $.ajax({
    url: ‘/Home/AutosugestSearch?q=’ + searchq,
    cache: false,
    success: function (html) {
    resultDiv.html(html);
    }
    });
    }

    //

    форма в HTML:

    В контроллере Home описываем метод AutosugestSearch, который отбирает нужные результаты в зависимости от строки:
    public PartialViewResult AutosugestSearch(string q)
    {
    //валидация входной строки
    if(!string.IsNullOrWhiteSpace(q))
    {
    //выбираем нужные данные из базы в зависимости от строки
    using (var db = new ImageKeeperContext())
    {
    var model = db.Users.Where(x => x.Login.Contains(substingInLogin)).Select(
    x => new SuggestedUsersViewModel
    {
    Login = x.Login,
    UserId = x.Id
    });
    }
    return PartialView(model.ToList(););
    }
    return PartialView(null);
    }

    SuggestedUsersViewModel это просто класс-модель. В mvc это стандарт. Передаем только нужные данные клиенту, а именно id и login.

    public class SuggestedUsersViewModel
    {
    public int UserId { get; set; }
    public string Login { get; set; }
    }

    Наконец в нашем Partial View формируем таблицу нужного формата для отправки клиенту:
    @model List
    @if (Model != null && Model.Any())
    {

    @foreach (var user in Model)
    {

    @Html.ActionLink(user.Login, “Page”, “Home”, new { userId = user.UserId }, null)

    }

    }

    И уж совсем на десерт css, чтобы на начальном этапе увидеть хоть какой-то результат и уже потом доверстать самому:
    .autosugTable
    {
    border: solid 1px black;
    }

    .active
    {
    background-color: skyblue;
    }
    остальные классы CSS придется настроить самим.

    P.S: Конечно всё это можно еще сильно оптимизировать, моё желание было показать суть, доказать, что всё что написал автор всё же работает в реальном-виртуальном мире:), пусть даже и с небольшими переделками. Плюс как плюшка – пример серверной стороны.

  13. Андрей

    После комментария заметил что весь HTML съелся, ну это впринципе было логично, так что выкладываю полную версию СЮДА:
    http://pastebin.com/QesQDSXB

  14. Макс

    А не проще сделать так?

    var block_ajax_search = true;
    function searchGo(top, left, count) {
        var ajax_timeout;

        if (block_ajax_search == true){
            block_ajax_search = false;
            clearTimeout(ajax_timeout);        
            ajax_timeout = setTimeout(function() {    
                var ajaxurl = …;
                $.ajax({
                type: "GET",
                url: ajaxurl,
                dataType:"jsonp",
                async: false,
                cache: false,
                success: function(data){    
                   …
                },
                complete: function() {
                    block_ajax_search = true;
                }
                });
            }, 600);
        }
    }

Leave a comment

 Comment Form