Контекстное меню на javascript: небольшое, но мощное

    Вы наверняка не раз видели javascript-реализации контекстных меню на базе популярных библиотек, таких как jQuery и prototype. А значит обязательно сталкивались с основными их недостатками: неудобностью API, большим количеством кода, требовательностью к ресурсам, любовью к генерации огромного количества html кода. В один прекрасный момент эти проблемы пересилили мою лень и я решил бороться с ними, поставив следующие задачи:
    • Минимум html кода, генерируемого для меню (зачем нам засорять ДОМ)
    • Лаконичность js кода для создания меню (API вызова без копипасты)
    • Оптимум гибкости при работе (многоуровневые, динамически модифицируемые меню)
    • Как можно меньше кода в реализации библиотеки (6302 байта в несжатом виде)
    • Минимальное количество jQuery-вызовов (чтобы можно было легко от них отказаться тем, кто jQuery не использует)
    • Inline-события где это возможно вместо биндов (меньше ресурсов сожрет)

    Контекстное меню

    UPD: разместил проект в google code, пользуйтесь, развивайте:
    svn checkout js-cmenu.googlecode.com/svn/trunk js-cmenu-read-only


    Функционал


    Подменю есть. Их вложенность теоретически не ограничена.
    Пункты меню можно делать недоступными (disabled=true), невидимыми (visible=false), можно динамически изменять caption, icon и добавлять новые пункты меню и подменю.
    Корректно работает у различных границ областей экрана, отрабатывается ситуация когда меню находится в скроллируемом диве (скролл вместе с элементом, вызвавшим меню).
    Радиоменю: выбор одного из пунктов меню.
    Несколько вариантов построения и дальнейшего поведения меню.

    Из соображений отсутствия необходимости следующие функции были изъяты: создание меню по ajax-запросу, вызов по правой кнопке (не везде работает), горизонтальное меню (крайне редко использовалось).

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


    Есть глобальная коллекция, куда собираются все-все меню (линейный список). Собственно меню представляет собой некий объект, содержащий информацию о своем поведении и состоянии, а также пункты меню. Пункт меню может содержать подменю.

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

    Одно меню можно фигачить во многие элементы на странице. От этого меню не размножится, но тем не менее будет знать, откуда его вызвали (это ведь принципиально в обработчике).

    Я старался сделать код максимально простым и понятным, чтобы добавление новых функций и удаление чего-то ненужного не представляло трудностей. К примеру, реализация радио-меню потребовала пять или шесть строчек и 10 минут времени. Модифицируйте код как хотите, но в комментах указывайте всё же ссылку автора.

    Примеры создания и вызова


    Меню можно создавать разными способами, в зависимости от степени извращенности меню, которое вы хотите в итоге получить. Самое простое — передать конструктору меню массив действий.
    var x = $.cmenu.getMenu([
    new menuItem('Назад', 'arrow_left',function(){history.back();}),
    new menuItem('!Вперед', 'arrow_right',function(){history.forward();}),
    new menuItem('Обновить','arrow_refresh',function(){location.href=location.href;})
    ]);
    $('.callMenu').bindMenu(x);
    $('#main_link').bindMenu(x);

    На выходе получите ссылку на готовое меню. Которое можно биндить куда угодно. Уже забиндили! Хотя нет, вру. Можно еще проще: забиндить к элементу массив действий.
    $('.callMenu').bindMenu([
    new menuItem('Назад', 'arrow_left',function(){history.back();}),
    new menuItem('!Вперед', 'arrow_right',function(){history.forward();}),
    new menuItem('Обновить','arrow_refresh',function(){location.href=location.href;})
    ]);

    Совсем забыл, можно еще проще: задавать вместо действий массив параметров этих действий.
    $('.callMenu').bindMenu([
    ['Назад', 'arrow_left',function(){history.back();}],
    ['!Вперед', 'arrow_right',function(){history.forward();}],
    ['Обновить','arrow_refresh',function(){location.href=location.href;}]
    ]);

    это равносильно вызову конструктора действия
    menuItem = function(caption,icon,execute,submenu)

    или просто заданию действия через json
    {
    caption:'Caption',
    icon:'может быть undefined, кстати, как и все остальные параметры',
    visible:true,
    disabled:false,
    execute:function(){},
    submenu:{объект-меню, массив действий или функция, создающая меню — об этом далее}
    }

    Это всё покроет основную массу задач. Но что, если нам надо меню, динамически меняющееся в зависимости от внешних факторов? Я много думал об этом, пробовал различные реализации, в итоге устоялась одна: передаем конструктору меню функцию. Эта функция будет вызываться всякий раз, когда надо показать меню.
    Внимание: важно!
    Для оптимизации работы меню в целом эта функция работает довольно странно. Она получает в качестве единственного параметра объект-меню в полное распоряжение. Вернуть она должна либо ложь (это будет означать, что меню не требует перерисовки), либо истину, либо массив действий. Но массив действий можно не возвращать, а просто записать его в член «a» объекта-меню — menu.a = [массив действий], это равносильно.
    Часто меню зависит не только от состояния окружения, но и от того, какой элемент меню вызвал. Для этого меню имеет член caller. Он содержит ссылку на DOM-элемент, вызвавший меню. Для подменю этот элемент будет ссылкой на дом-элемент родительского меню, поэтому имеет смысл смотреть на член parentMenu, содержащий ссылку на меню-родителя.

    Типичная функция выглядит так:
    menuGenerator = function(menu){
    	if(!menu.a){
    		// начальная инициализация меню
    		return true; // нужна перерисовка
    	}
    	if(myVarChanged()){ // что-то случилось в объектной модели
    		menu.a.doAction.disabled = myVarValue();
    		return true; // нужна перерисовка
    	}
    	if(menu.caller.id=666){
    		menu.a.doAction.visibe = false;
    		return true; // нужна перерисовка
    	}
    	return false; // всё по-прежнему, перерисовка не нужна
    }


    Пару слов про объект-action. Самый важный метод в нем — это execute. Этот метод будет вызван при клике на пункте меню. Он принимает три параметра. Первый — сам объект-действие, второй — меню, третий — массив-цепочка вызовов меню (для сложных многоуровневых меню может пригодиться).
    Менее важный член объекта-действия — submenu. Тут может быть массив действий, либо функция-генератор меню.

    Для радио-поведения меню задайте в объекте меню свойство menu.type = 'radio', и два метода: set(str) и get

    Посмотрите пример, там раскрывается тема радио-меню

    И последнее. Вместо бинда можно использовать конструкцию посложнее. Это позволит избежать мусора в доме в виде событий несуществующих элементов. Да, я говорю про инлайн-вызовы. Есть в классе-фабрике-меню метод $.cmenu.getCaller(menu) или «перегруженный» $.cmenu.getCaller(event,menu), который вернет строчку из параметров типа такой:
    onclick="$.cmenu.show(0,this);$.cmenu.lockHiding=true;" onmouseout="$.cmenu.lockHiding=false;"
    Эту строчку можно зафигачить в элемент.

    Если знаете реализации лучше, пожалуйста, не скрывайте — высказывайтесь.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 47
    • 0
      Как здорово! ) и быстро !))
      спасибо большущее за информацию, непременно к осмысливанию
      • 0
        Пожалуйста. Пользуйтесь, модифицируйте как надо. Лицензия MIT.
        • 0
          А почему не BEER-WARE? (:
          • 0
            Для хабралюдей из Казани можно и beer-ware (:
      • 0
        По идее, когда курсор уходит с пункта с подменю, то подменю должно скрываться.
        • 0
          так и есть. только не при уходе, а при наведении на другой пункт. по таймеру
          • 0
            Ага, это довольно непривычно :)
            • 0
              Я делал по образу и подобию меню в фаерфоксе. Там так. В Опере иначе… Кстати идея! Можно менять это поведение в зависимости об бразуера :)
              • 0
                Хм… я у себя не ощущаю задержки в FF3 на WinXP и разницы с Оперой тоже не замечаю. Возможно, потому что у меня в винде убрана задержка вылезания меню.
                • +1
                  Таймер тоже стоит оставить — события выхода не всегда корректно отслеживаются.
          • 0
            Cool, thnx
            • +1
              сделайте иконки на спрайтах
              • 0
                Думал об этом, но до реализации пока руки не дошли — разные меню используют разные иконки, поэтому монолит может быть довольно большим. Но в туду этот пункт есть.
              • 0
                Интересная штука. Подгючевает в IE, но, работает.
                • 0
                  Капец просто. Хотел посмотреть в ие, обнаружил интересный факт. У меня эксплорер открывает странички с ip-адресом в Опере!
                • +12
                  оффтоп: было бы неплохо чтобы автору топика добавлялась карма за то, что другие заносят его пост в избранное, а то пост полезный, а плюсануть не могу… пусть хоть так опосредованно перепадает благодарность :)
                  • –1
                    Эх, заминусут Вас щас :) Знаете ведь — не любят на Хабре разговоров о карме.
                    • +7
                      Да мне по барабану, если честно.
                      Просто хочется выразить благодарность принятыми в данной системе способом, а не могу :)
                  • 0
                    Хм… у меня по клику правой клавигей ничего не происходит… А вот о клику левой — открывается)
                    Firefox 3.0.3, Kubuntu 8.04.1…

                    И еще — насколько я помню, Опера не отлавливает клик правой клавишей. Или уже отлавливает?
                    • 0
                      Так и должно быть, чтобы сохранить возможность для пользователя открыть дефолтное меню браузера.
                      • 0
                        Не отлавливает. Именно поэтому от поддержки правой кнопки мыши я отказался. Хотя никто не запрещает биндить по любому событию (через getCaller(string event,menu menu))
                        • 0
                          В настройках Оперы есть пункт «Allow script to receive right click», но я как-то по традиции снимаю галочку.
                        • +1
                          Заметил особенность в Опере, если выбрать в меню пункт «Назад...», то после этого меню перестаёт вызываться.
                          • 0
                            Да, спасибо. Щас тоже заметил. Видимо для оперы history.back(); когда назад некуда — это ошибка.
                            • 0
                              Видимо, это странность Оперы: try-catch эту ошибку не ловит. Если есть куда переходить — переходит. Если нет — убивает скрипты. Что ж. Будем боросться.

                              Запуск другим потоком тоже ничего не дал. Если есть идеи — пишите.
                              • 0
                                history.length не поможет?
                                • 0
                                  Помогает не всегда. Иногда при непустой истории history.length равен нулю. В Опере только.
                            • 0
                              А сложно под mootools всё это переделать?
                              • 0
                                Абсолютно не сложно. jQuery там всего несколько вызовов. Люди, знакомые с mootols без труда заменят из соответствующими. К сожалению, я к таковым не отношусь, поэтому пока только jQuery. Если сделаете — давайте ссылку, добавлю в пост.
                              • 0
                                Опечаточку поправьте)
                                «Если знаете реАлизации лучше»…
                              • 0
                                В IE6 не работает (показывается за краями видимой области)

                                А можно ли как-то динамически подгружать пункты меню. Например в базе хранится некий список с URL по который можно переходить и когда юзер кликает на «вызов меню», то этот список из базы погружается в меню…
                                • 0
                                  С ие разберемся, ага. Динамическая подгрузка была реализована в ранней версии меню. Там можно было задавать меню строкой-урлом, по которому шел запрос. На практике оказалось не нужно и эту ветку быстро забыли. Фактически игра не стоит свеч: для подобных меню правильнее загружать ресурсы заранее, иначе это ведет к увеличению времени взаимодействия с пользователем и лишним гет-запросам.
                                  • 0
                                    >Фактически игра не стоит свеч:…
                                    Нууууу, может быть. Хотя если это реализуемо, что почему бы и нет. Я пересмотрел очень много контекстных меню — и у всех один, для меня, недостаток — надо заранее задавать все пункты меню. Я бы предпочел сделать лишний ajax запрос для генерации меню, чем заранее загружать его, IMHO.
                                    • 0
                                      А что мешает сделать это ручками?
                                      $.getJSON('datasource.php?q=GetAllDataInOneRequets',function(json_response_object){
                                        $('#menu_click_here').bindMenu(json_response_object.menu1);
                                        $('#menu_click_me').bindMenu(json_response_object.menu2);
                                        $('.blablablaMenu').bindMenu(json_response_object.menuBlaBlaBla);
                                      }
                                      

                                      Так мы грузим всё одним запросом.
                                • 0
                                  спасибо — будем пробовать
                                  • 0
                                    Большой привет Казани! Очень хочу там побывать! (сорри за офтоп).
                                    Теперь по делу: если не ошибаюсь, то в опере нельзя использовать контекстное меню? или все таки ошибаюсь? позже протестирую.
                                    • 0
                                      Можно. Там в примере глючок закрался паршивенький, а так всё нормально.
                                    • 0
                                      Автор! Настоятельно советую перенести вызов js в конец файла, или сделать его как-то отложенно. Что меня всегда раздражало — так это привычка ставить js в начало файла, из за чего ощутимо замедляется загрузка((( Неприкольно смотреть на белвый экран((
                                      Разница в скорости загрузки с и без поддержки js видна невооруженным глазом.

                                      А меню — хорошее, только стили бы поменять умолчальные.
                                      • 0
                                        Можно еще добавить стрелки вверх вниз. Там элементарно делается. Пример jotsky.com.
                                        • 0
                                          Может уже слишком поздно и мой мозг спит, но я не увидел на сайте примера, хотя интуитивно догадываюсь, что речь идет о стрелочках для скроллирования мега-больших меню, не помещающихся на экран.
                                          • 0
                                            Я думаю речь скорее об управлении с клавиатуры.
                                        • 0
                                          идея хорошая, но возможно динамические меню стоит реализовывать через ajax.
                                          фиксированные меню лучше сразу сгенерировать в html, завернуть в невидимый блок и вызывать по требованию.
                                          <div style="display: none;"> menu </div>
                                          

                                          • 0
                                            Фиксированные меню рендерятся по требованию. Довольно часто их вообще не вызывают. Всё-таки контекстными меню (по крайней мере наши юзеры) пользуются не так часто. Но в принципе на инит поставить вызов render() дело пяти секунд. Раньше, кстати так и было, но пришлось убрать в силу указанных выше причин.
                                          • 0
                                            а я бы привязал меню к курсору, а то ведь, если область клика большая то меню вылазит далековато от меню (собственно говоря — в примере так и есть), а так это было бы более похоже на контекстное меню, где кликнул — там и вылезло.

                                            саму функцию попробовал — интересная, вот теперь думаю использовать, или свое сделать, заточенное под мои нужды.
                                            • 0
                                              Отличный пример!!!
                                              • 0
                                                Вах! Error 404 по всем ссылкам! Верните все назад!

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