Pull to refresh

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

Reading time 5 min
Views 14K
Вы наверняка не раз видели 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;"
Эту строчку можно зафигачить в элемент.

Если знаете реализации лучше, пожалуйста, не скрывайте — высказывайтесь.
Tags:
Hubs:
+74
Comments 47
Comments Comments 47

Articles