Я пойду своим путем или нестандартное меню

в 20:41, , рубрики: javascript, JS, меню, метки: ,

Прочитав заголовок, кто-то наверно подумал, что сейчас будет очередной велосипед, и будет в принципе прав. Но иногда ничего не остается, как велосипедить, когда среди альтернатив сходу не находится ничего подходящего. Так случилось и у меня.

Задача

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

Понятно, что первым делом возникла мысль поискать нечто подобное в куче плагинов к Jquery. Но проблема обнаружилась сразу после первых поисковых запросов. Дело в том, что большинство плагинов подразумевают вложенность <ul> в <li> для уровней больших либо равных двум. Это противоречило уже отверстанному макету, да так сильно, что уже маячила перспектива его переверстки.

Меню в макете выглядело примерно так:

<ul class="lvl_1 lvl">
    <li class="1">...</li>
    <li class="2">...</li>
    <li class="3">...</li>
    <li class="4">...</li>
</ul>
<ul class="lvl_2 1 lvl">
    <li class="1">...</li>
    <li class="2">...</li>
    <li class="3">...</li>
    <li class="4">...</li>
</ul>
....

Списки существовали фактически самостоятельно друг от друга и надо было это как-то сохранить. В голову не пришло ничего, кроме как писать свое решение с нуля.

Правила и требования

Я условился об именовании. В атрибуте класса <ul> у меня сначала идет имя уровня, а затем все что еще необходимо. Кроме того, в этом же атрибуте и меня идет цифра-идентификатор, которая означает какой именно элемент <li> верхнего уровня откроет данный список. Соответственно, в <li> в качестве класса стоит эта самая цифра.

(надо заметить, что на уровне верстки эта цифра весьма условна, но при реализации на уровне движка на эту роль замечательно подходошли id сущностей, которые вносятся в меню)

Реализация

Приведу получившийся код. Каждый обработчик/блок кода снабжен комментарием, но ничего сверхъестественного там нет.

$(function() {

	/*-----------обрабатываем поведение ссылки на первый уровень---------
	-------------добавляем пару классов для стилизации и выводим первый уровень-------------*/

	$('li.catalog > a').hover(function(){ // можно заменить ссылку на нужную
		$('ul.lvl_1').addClass('hovered');
		showup('ul.lvl_1');
		$(this).addClass('show_1');
	},function(){
		$(this).removeClass('show_1');
	});

	/*----------обрабатываем наведение на что-либо внутри списка любого уровня вложенности
	------------логика общая : вычисляем текущий левел по имени класса списка, а затем скрываем все что глубже
	------------показываем только левел+1, причем с таким классом, на который указывает элемент li
	------------это позволяет делать независимые списки на одном уровне-------------------------------------*/

	$('ul.lvl li > *').hover(function(){
		var who = $(this).parent().attr('class');
		var who_lvl = $(this).parent().parent().attr('class');
		$(this).parent().parent().addClass('hovered');
		var classes = who_lvl.split(' ');
		var level = classes[0].split('_');
		for(i=parseInt(level[1],10)+1;i<5;i++){
			$('ul.lvl_'+i).removeClass('hovered');
			showdown('ul.lvl_'+i,true);
		}
		if($('ul.'+who+'.lvl_'+(parseInt(level[1],10)+1)).length){
			showup('ul.'+who+'.lvl_'+(parseInt(level[1],10)+1));
		}
		
	},function(){
		$(this).parent().parent().removeClass('hovered');
	});

	/*----------отслеживаем координаты мыши------------*/
	
	var mX;
	var mY;
	$(document).mousemove(function(event) {
		mX = event.pageX;
		mY = event.pageY;
	});

	/*----------отслеживаем состояние мыши относительно уровней меню
	------------для каждого списка меню прогоняем проверку
	------------выясняем координаты границ и уровень каждого списка
	------------если это не первый и не последний уровень, то проверяем открыто ли что-то из меню уровня выше и ниже
	------------затем уровень ниже и выше закрыты, то и текущее меню надо закрыть (1)
	------------если уровень выше закрыт, мышь уведена от текущего меню и мышь не наведена на уровень ниже, то тоже закрываем (2)
	------------если выполянется аналогичное (2), но наоборот, то тоже закрываем (3)
	------------для меню последнего и первого уровня логика своя, но похожа----------------------------*/	
	
	function closer(){
		$('ul.lvl').each(function(){
			if($(this).css('display')=='none')return;
			var moX = $(this).offset().left;
			var moY = $(this).offset().top;
			var moW = moX + $(this).outerWidth();
			var moH = moY + $(this).outerHeight();

			var who_lvl = $(this).attr('class');
			var classes = who_lvl.split(' ');
			var level = classes[0].split('_');
			var ilvl = parseInt(level[1],10);
			if(ilvl!=1 && ilvl!=4){
				var nonminus=1;
				$('ul.lvl_'+(ilvl-1)).each(function(){
					if($(this).css('display')!='none') nonminus=0;
				});
				var nonplus=1;
				$('ul.lvl_'+(ilvl+1)).each(function(){
					if($(this).css('display')!='none') nonplus=0;
				});
				
				if((nonminus && nonplus)){ // (1)
					$(this).removeClass('hovered');
					showdown(this);
				}
				if(nonplus && ((mX<moX || mX>moW) || (mY<moY || mY>moH)) && !$('ul.lvl_'+(ilvl-1)).hasClass('hovered')){ // (2)
					$(this).removeClass('hovered');
					showdown(this);
				}
				if(nonminus && ((mX<moX || mX>moW) || (mY<moY || mY>moH)) && !$('ul.lvl_'+(ilvl+1)).hasClass('hovered')){ // (3)
					$(this).removeClass('hovered');
					showdown(this);
				}
			}
			if(ilvl==1){
				if(!$('li.catalog > a').hasClass('show_1') && $('ul.lvl_2').css('display')=='none' && ((mX<moX || mX>moW) || (mY<moY || mY>moH))){
					$(this).removeClass('hovered');
					showdown(this);
				}
			}
			if(ilvl==4){
				if(((mX<moX || mX>moW) || (mY<moY || mY>moH)) && !$('ul.lvl_3').hasClass('hovered')){
					$(this).removeClass('hovered');
					showdown(this);
				}
			}
		});
	}
	
	/*-------сам setInterval, частоту настраивается по ситуации----------*/

	viewer = setInterval(closer, 400);
	
	/*------------помощник для показа меню-------------*/

	function showup(obj){
		if($(obj).css('display')=='none')
			$(obj).show();
	}

	/*----------помощник для скрытия меню
	------------хитрее тем, что возможны несколько меню одного уровня
	------------скрывать надо только, если есть нескрытое-----------*/ 
	
	function showdown(obj){
		var nothide=0;
		$(obj).each(function(){
			if($(this).css('display')!='none') nothide=1;
		});
		if(nothide){
			$(obj).hide();
		}
	}
});

Этот код применялся для меню вида:

<ul class="nav">
	<li class="catalog">
		<a href="#">Каталог</a>
		<ul class="lvl_1 lvl">
			<li class="1">...</li>
			<li class="2">...</li>
			<li class="3">...</li>
			<li class="4">...</li>
		</ul>
		<ul class="lvl_2 1 lvl">
		    <li class="1">...</li>
			<li class="2">...</li>
			<li class="3">...</li>
			<li class="4">...</li>
		</ul>
		<ul class="lvl_3 2 lvl">
			<li class="1">...</li>
			<li class="2">...</li>
		</ul>
        <ul class="lvl_4 1 lvl">
			<li class="1">...</li>
			<li class="2">...</li>
			<li class="3">...</li>
		</ul>
	</li>
	<li><a href="#">Новинки</a></li>
	<li><a href="#">Партнерам</a></li>
	<li><a href="#">Контакты</a></li>
</ul>
P.S.

В идеале хотелось бы перевести это из разряда «велосипед для себя» в «плагин для всех». Особенно, если учесть, что у меня лично периодически возникают задачи подобного рода.
Хотелось бы избавиться от привязки кода к количеству уровней вложенности, сделать более гибкую систему привязок и уйти от правил именования <ul>, чтобы дать большую свободу.
Но руки, пока, до этого не дошли. Хотя искренне надеюсь, что этот пример поможет кому-то в трудную минуту или сподвигнет на написание более гибкого плагина, чем мне доводилось находить на этапе поиска :)

Автор: MUTOgen4eg

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js