RTM Context Autocomplete Menu

    Однажды я зашел на rememberthemilk.com и понял, что хочу такое же контекстное autocomplete меню в свой проект. В результате получился небольшой jquery плагин, который хочу презентовать в этом посте. Работает в ie6+, opera, safari, firefox, chrome (тестировал в последних версиях). В кратце расскажу в чем суть «контекстного» меню в RTM-стиле.

    Это меню присоединяется к input-элементу, но, в отличие от обычных autocomplete меню, оно «всплывает» не для ввода всего значения элемента, а для какой-то логической части поля ввода. При этом меню позиционируется непосредственно под автодополняемым текстом. Вот как это выглядит:

    image

    Лицензия проекта — MIT / beerware.
    Скачать библиотеку с примерами можно тут: js-context-autocomplete.googlecode.com/files/js-autocomplete-v5.tar
    Последнюю ревизию забираем тут: svn checkout js-context-autocomplete.googlecode.com/svn/trunk js-context-autocomplete-read-only
    Кому интересно поучаствовать в проекте — пишите в личку.
    Временное online-demo (upd)

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


    Примеры использования


    1. var $input = $('#text_input');
    2. // обычный автокомплит (не контекстный)
    3. $input.autocomplete(['Питание', 'Бытовые расходы', 'Машина', 'Здоровье', 'Счета']);
    4.  
    5. // короткий формат записи:
    6. //     принимает объект, в котором ключом может быть либо один символ,
    7. //     тогда соответствующее меню будет появляться после этого символа,
    8. //     либо регулярное выражение вида '^something(match)$' заданное строкой
    9. //     (будет входным параметром для конструктора RegExp)
    10. $input.autocomplete({
    11.     '^\\d+\\s+(.*)$': categories,
    12.     '^\\d+\\s+.*?\: (.+)$': notes,
    13.     '#': habra_tags, // при наборе символа # показываем хабра-тэги
    14.     '@': places, // при наборе символа @ будет отображаться меню с локациями
    15.     '!': ['1', '2', '3'] // при наборе символа ! будет отображаться меню с приоритетами
    16. });
    17.  
    18. // расширенный формат записи:
    19. //     принимает массив объектов
    20. //     в которых обязательными являются члены regex и items
    21. $input_with_suffix.autocomplete([
    22.     {
    23.         regex: /^\d+[.,]?\d*\s+(.*)$/,
    24.         items: categories,
    25.         suffix: ': ' // этот суффикс будет автоматически добавляться при выборе из меню
    26.     }, {
    27.         regex: /^\d+[.,]?\d*\s+.*?: (.+)$/,
    28.         items: notes
    29.     }
    30. ]);
    * This source code was highlighted with Source Code Highlighter.


    Известные баги


    1. после выбора пункта меню курсор перемещается в конец всего текста, ожидается перемещение в конец вставленного фрагмента
    2. после выбора кликом мышки поле ввода не получает фокус обратно
    3. короткое поле для ввода и длинный текст — ошибка позиционирования меню
    4. слепая вера в то, что элементы меню — строки (добавить проверку и приведение к строке)
    5. autocomplete=«off» не включено по умолчанию (необходимо включить)
    6. отсутствует лимит на количество элементов, отображаемых одновременно, для слишком длинных меню

    Фичи для последующей реализации


    1. ajax-загрузка элементов меню
    2. сложный формат элементов меню (эмуляция ), доп. события
    3. возможность настройки стиля каждого элемента меню
    4. зависимость меню от сторонних данных (в этом же елементе или на странице)

    Зачем это всё?



    Как это работает?



    Изначально меня заинтересовало в RTM autocomplete то, каким образом можно получить расстояние в пикселях от начала поля ввода до текущей позиции курсора. Именно это подтолкнуло меня на начало работы. Оказывается, что решить эту проблему можно довольно просто: достаточно вычислить ширину скрытого div'а, заполненного текстом с тем же стилем, что и в меню. К сожалению, это решение порождает баг номер 3, но меня это не особо волнует пока. Возможно кто-то может предложить лучшее решение?

    Когда всплываем?


    Следующий интересный момент — определение позиции для которой должно всплывать меню. Для «родного» RTM-меню свойственно выпадать сразу после ввода ключевых символов (@ для локации, # для тега,! для приоритета и т.д.), однако не хотелось ограничиваться таким поведением и предоствавить свободу показывать меню для любых случаев: хоть для ввода тэгов через запятую, хоть каких-то более для сложных форматов. Тут на помощь приходят регулярные выражения. Для каждого множества элементов назначается регулярное выражение, с которым должна совпасть введённая строка. Когда строка совпала с регулярным выражением — отображаем меню начиная с первого совпавшего символа в автодополняемой строке. И сразу пример, чтобы понять о чем я.

    Пусть мы хотим чтобы в поле для ввода можно было ввести сумму в рублях и статью расхода, куда деньги потрачены. При этом мы хотим, чтобы после окончания ввода цифр появлялось меню. Пишем простейшее регулярное выражение для такого поведения: /^\d+\s+(.*)$/. Теперь когда мы введем число и пробел, всплывет меню.

    Усложним ситуацию. Пусть мы уже ввели значение и меню исчезло, но потом передумали, удалили несколько символов чтобы выбрать другой пункт меню. Мы хотим чтобы меню всплыло не рядом с курсором, а от начала слова, котрое мы изменяем. Для этого в нашем регулярном выражении мы «ловим» слово используя такую конструкцию (.*). Кстати, если бы мы хотели, чтобы меню всплывало после первого совпавшего символа слова, очевидно эта конструкция должна была бы выглядеть как (.+). Таким образом достаточно просто можно настраивать сколь угодно сложные способы поведения меню.

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

    1. $input_just_for_tags.autocomplete([
    2.     {
    3.         // первый тег (с начала строки)
    4.         regex: /^([^,]+)$/,
    5.         items: habra_tags
    6.     },
    7.     {
    8.         // не первый тег (то что до последней запятой перед курсором)
    9.         regex: /^.*,\s+([^,]+)$/,
    10.         items: habra_tags
    11.     }
    12. ]);
    * This source code was highlighted with Source Code Highlighter.


    Есть варианты, как обойтись одним регулярным выражением и удовлетворить требованию, чтобы автодополняемый тэг ловился в первом совпавшем фрагменте?

    Рутина


    На всё вышеозначенное ушло совсем немного времени — один вечер (два-три часа). После этого оно работало в FF и как-то странно в Opera, отказывалось слушаться клавиш up-down-enter-esc в msie и webkit. А поделиться с тем что у меня получилось хотелось бы со всеми желающими. Подзуживало еще то, что в rememberthemilk все работает одинаково во всех браузерах. Так в чем же проблема? Тезисно:

    1. key events (keypress, keydown) — очень повеселило, как разные браузеры работают с клавиатурными событиями. Помогает это и это (9: JavaScript Events)
    2. selectionStart, selectionEnd — msie не знает об этом. Но там же createRange есть.
    3. jquery.each оказалось, что этот замечательный метод работает немного по-разному в ie и не ie. Мелочь, но это тема для другого разговора, потому что интересно почему так, но разобраться пока не было шанса.

    Вместо резюме


    Code review required ;)
    Поделиться публикацией

    Похожие публикации

    Комментарии 33

      +2
      таки онлайн демки не хватает
      0
      интересно будет проследить за дальнейшей разработкой,
      онлайн демка приветствуется
        +1
        Странно, но у меня почему-то не убирается автокомплитер, если не выбрать значение и перейти к следующему полю
          0
          Думаю, вы правы, на потерю фокуса можно забиндить сокрытие меню.
            0
            Именно! А так все здорово, уже есть идеи для использования)))
            Кстати, а как будет вести себя в textarea?
              0
              Делал только для input. Для textarea в принципе тоже можно, но там надо еще и высоту считать, что, в принципе, решается тем же способом, только с указанием max-width. Поэтому с кроссбраузерностью будут проблемы.
                0
                Поэтому и возник вопрос. А если как-нибудь по высоте строки самой подсчитать? Например, длину мы ведь можем подсчитать, так? В конце длины добавлять невидимый символ, а потом по нему считать количество строк и умножить на высоту строки. Почему-то именно такая идея возникла
                  0
                  Мы не знаем где браузер расставит переводы строк, да и незачем знать в общем-то. Достаточно содержимое textarea отправить в скрытый div и посмотреть какая получилась высота в пикселях. Только вот надо учесть два момента: 1. ширина скроллбара, 2. содержимое textarea может скроллиться.
                    0
                    Высчитать ширину скроллбара можно и тем же дивом (overflow='scroll'). Правда придется измерять два раза.
                    А вот по поводу решения второй проблемы надо подумать.
                      0
                      По крайней мере в ff есть textarea.scrollTop
                        0
                        Согласен. И для Opera тоже сработает, по-моему у него только с IE глюк
            0
            Добавил фикс в svn. Спасибо за участие :)
            0
            поправьте на скриншоте «аксессауры»
              –2
              А насколько это сложно написать без использования jQuery? Много переделывать?
              А то так вы оставляете кучу людей без решения и еще группу заставляете использовать jQuery из-за одного плагина
                0
                Думаю, часа два нужно чтобы переписать без jquery. Максимум три. Там меньше трёх сотен строк.
                  +1
                  Действительно, пришел человек, написал статью, заставил прочитать, мало того, оставил людей без решения на jQuery и ещё заставляет использовать этот самый jQuery.
                  Редиска!
                  +1
                  Классное меню, молодец!
                    0
                    Beerware — отличное определение.)
                      0
                      Офигеть, <habrauser=kutanov> на аналог этого для task.ly убил уже три вечера. =) Правда, на чистом js.

                      Приходите к нам фронтенд-девелопером, а? :)
                        0
                        да-да… манифик. в моей версии как раз перечисленных проблем нет, надо будет слить их. займусь как время появится.
                        0
                        jquery.bassistance.de/autocomplete/demo/
                        Смотрим пример с Tags (local).
                        Рекомендую

                          +1
                          Меню появляется всегда в одной позиции. Мне было интересно показать именно контекстный характер меню.
                            0
                            Теперь понял отличие :) Успехов вам.
                            +1
                            Весьма глючная хрень, если начинаешь редактировать внутренние таги, то всё перемешивается.
                            +1
                            Спасибо! В закладки.
                            Насчет разницы в работе с selection в IE и остальных — сочувствую. Как-то приходилось писать эмуляцию w3c-range для IE :)
                              0
                              Скрипт не работает, если jQuery подключен в режиме совместимости (jQuery.noConflict();).
                              На этот случай лучше все вызовы $ заменить на jQuery.
                                0
                                Не согласен. Скрипт работает в любом случае. Если в sandbox.html поставить вызов типа $jq = jQuery.noConflict(); тогда действительно работать перестанет, но это уже проблема не компонента, а юзера. Сам же плагин использует стандартный шаблон (function ($) {})(jQuery);
                                  +1
                                  да, ваша правда — сам плагин использует правильную конструкцию.
                                  однако, видимо, какие-то евенты обрабатываются неверно:
                                  1. включить jQuery.noConflict();
                                  2. подключить плагин — все заработает, саджест будет выдаваться.
                                  3. при наведении мышки на пункт выпадающего меню вылетает ошибка: $ is not a function

                                  Я когда подключал не стал сильно разбираться что там не так и просто заменил все $ на jQuery :)

                                  PS. спасибо за разработку, использую этот плагин сейчас в проекте.
                                    0
                                    Вот за этот багрепорт спасибо. Действительно, в одном месте есть inline вызов (на onmouseover) с использованием $. В 10-й ревизии исправлено. Должно работать нормально.
                                0
                                Круто, в нашем bug tracker используется похожий компонент для ввода поисковых запросов.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое