Выпадающее многоуровневое меню. CSS + javascript

версия для печати

Теоретическая задача: на Javascript создать единый код для обработки многоуровневых меню, выпадающих в любом направлении (слева/справа или снизу/сверху). Т.е. решить не частную задачу, а общий случай. Вообще задача изначально носила практический характер. Нужно было прикрутить менюшку к одному коммерческому проекту, причем времени на разработку не было. Пошарился в инете, найденные решения меня не устроили. Либо заточены под один вид меню, либо ограничения по уровням. Нашел даже совсем экзотический вариант, на дли-и-и-инном css-управлении. Все-таки пришлось делать свое, универсальное.

Пример работающих менюшек здесь, скачать проект здесь. Не обращайте внимание на общее оформление примера. Как я уже говорил, задача изначально была практической :)

В разработке нужно учесть:

  1. кроссбраузер. Беру во внимание то, что есть под рукой: Opera 11, FF 18, IE 6-8 (через IETester);
  2. у юзера возможно отключен javascript, тогда нужно как-то иначе меню показывать или не показывать вообще;
  3. меню может быть c любым количеством уровней;
  4. курсор может покинуть зону меню с любого уровня, тогда нужно корректно свернуть все дерево;
  5. видимое меню должно располагаться поверх всех других блоков на сайте;
  6. блоков меню может быть на странице несколько, причем в разных направлениях. Принимаем допущение: ветки одного блока меню могут расти только в одном направлении. Например, если меню расположено слева, то все вложенные уровни открываются только вправо, назависимо от того, входят они на экран или нет.
  7. код нужно сделать максимально независимым от id элементов, их размеров и положения на странице. Так же желательно не привязываться на уровень вложенности дочерних элементов.
  8. [Не сделал] Интересная мысля: в jQuery есть встроенная функция hover, и расширение hoverIntent. Расширенная версия помогает избежать срабатывания при мимолетном прохождении мышки (функция сработает при определенных заданных параметрах - скорости движения курсора в пикселях/секунду, задержкой курсора и т.д.) Как бы такое реализовать на чистом javascript в своем исполнении? Думаю использовать setTimeout() и clearTimeout(). По таймауту проверить, что под курсором и в случае необходимости открыть меню. Не стал делать, т.к. нет времени.

Подуровни меню оформляем в div-ках, позиционировать будем относительно родителя, используя CSS-свойство Position: relative|absolute. С этим свойством будут траблы в IE6, учтем это. На javascript ловим события mouseover, mouseout. HTML-код будет примерно таким (вертикальное меню):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
...
<div id='menu'>
<h5>Первое меню</h5>
<ul>
<li><a href=''>Menu1</a></li>
<li onmouseover='toggleMenu(event,this)' onmouseout='parentMenuOut(event,this)'>
  <a class='subm0' href=''>Menu2</a>
  <div class='submenu' onmouseout='toggleMenu(event,this)'>
     <ul>
      <li><a href=''>submenu21</a></li>
     <li><a href=''>submenu22</a></li>
     <!-- другие пункты подменю -->
        ...
     <!-- еще уровень меню -->
     <li onmouseover='toggleMenu(event,this)' onmouseout='parentMenuOut(event,this)'>
          <a class='subm0' href=''>Menu2nn</a>
            <div class='submenu_l2' onmouseout='toggleMenu(event,this)'>
              
<ul>
              <li><a href=''>
submenu2nn1</a></li>
                ... <!-- другие пункты/уровни меню -->
                </ul>
         </div>
        </li></ul>
  </div>
</li>
...   <!-- другие пункты корневого меню -->
</ul>

<h5>Второе меню</h5>
<ul>
...   <!-- новая менюшка -->
</ul>
</div>

Пояснения. CSS-класс 'subm0' нужен, чтобы прицепить картинки стрелок на пунктах меню, имеющих подменю. Классы 'submenu*' используется для браузеров с отключенным javascript. Таких классов будет несколько, по количеству уровней меню. При этом все уровни вертикального меню выстраиваются лесенкой друг под другом. В js-коде все эти классы заменяется на один специальный. Для горизонтального меню ничего не придумал, поэтому юзер без javascript его вообще не увидит. При желании можно в парном теге <noscript> оформить какой-нить перл.

В строках 9-25 описан первый подуровень меню. Он "завернут" в div-ку. Любые подуровни описываются аналогично. Строить меню необязательно на тегах <ul> и <div>. Можно использовать таблицы и слои или вообще только div-ки. Теоретически, реализация при этом изменится незначительно.

Стиль CSS (частично):

#menu {
 float: left;
    width: 200px;
   position:relative;
  height: 1%; /*поправка для IE6. Без нее горизонтальные подменю неправильно располагаются*/
}
#hmenu a {display: block; height: 24px;} /*высота в строке ниже должна быть такая же*/
/*Хак IE6. Иначе пункты меню, имеющие вложения, становятся выше остальных*/
* html #menu li, * html #hmenu li {height: 24px;}
div.submenu{
   position:relative;
  left:10px;
  width: 190px;
}
div.submenu_l2{
   position:relative;
  left:20px;
  width: 180px;
}
div.js_submenu{
   visibility:hidden;
  position:absolute;
  width: 200px;
}

Баги IE6 исправляются указанными свойствами. Если у вас не будет горизонтального меню, то строка (5) не нужна. Хак в (9) можно переписать, как общее свойство для всех браузеров. Но тогда придется решать проблему слопывания отступов (см. "Верстка веб-страниц", В.Мержевич, 2010, с.119). Я выбрал меньшее из зол.

Три блока стилей div - это собственно поддержка браузеров без javascript и общий стиль подменю при наличии js.

Наконец самое интересное - код скрипта (практически весь):

function getNodeIdx(obj){
   var i=0;
    while (obj.childNodes[i].nodeName!='DIV') i++;
    return i;
}

function toggleMenu(event,obj){
   event=event||window.event;
  if (event.type=='mouseover'){//курсор над элементом
       var idx=getNodeIdx(obj);
        //Если первое подменю вызвывается из div, значит блок горизонтальный.
       //Для горизонтального меню - другие координаты первого подуровня.
       if (obj.nodeName=='DIV'){ //первое подменю размещаем прямо под корневым уровнем.
          obj.childNodes[idx].style.top=(obj.offsetTop+obj.offsetHeight)+'px';
          obj.childNodes[idx].style.left=(obj.offsetLeft-1)+'px';
       }
       else {                    //любые другие уровни подменю смещаем вниз влево
          obj.childNodes[idx].style.top=(obj.offsetTop+obj.offsetHeight/2)+'px';
            obj.childNodes[idx].style.left=(obj.offsetWidth*0.80)+'px';
       }
       obj.childNodes[idx].style.visibility='visible';
       obj.style.backgroundColor='#dbfbcc';
  }
   else{                        //курсор покинул элемент
       obj.style.visibility='hidden';
        obj.parentNode.style.backgroundColor='transparent';
   }
}

//Событие родительского меню. Мышь покидает элемент
function parentMenuOut(event,obj) {
    event=event||window.event;
   var relTrgt = event.relatedTarget || event.toElement;//W3C || 
IE
    var idx=getNodeIdx(obj);
    if (relTrgt!=obj.childNodes[idx]){ //Если элемент-получатель мыши не соответствует дочернему подменю, то скрываем подменю

n       obj.childNodes[idx].style.visibility='hidden';
        obj.style.backgroundColor='transparent'; }
}

Это будет сложно объяснить %) Рассматриваем только пункты меню с назначенными обработчиками событий. Назначение обработчиков можно сделать более профессионально, например через addEventListener(), но так нагляднее.

Строки 9-23: когда мышь наводится на пункт меню, функция getNodeIdx() ищет ближайщего потомка определенного типа, т.е. блок с подменю. Затем расчитывается его позиция и переключается видимость. Контроль за сокрытием блока ведется самим подменю через назначенный обработчик onmouseout().

Строки 24-26: когда мыша покидает блок подменю, он прячется, подсветка родительского пункта меню отключается.

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

Cамое офигительное: всплывающие события (Bubbling). Благодаря этой модели поведения скрывается всё дерево меню, когда мышь покидает его с любого подуровня. Поскольку на каждом дочернем блоке назначен обработчкик события onmouseout(), то его всплытие вызывает каскадное исчезновение уровней меню.

Следующая часть статьи не обязательна к прочтению.

Это же поведение, bubbling, приводит к лишнему "миганию" менюшками. На нормальной машине это незаметно, на медленной возможно будет некрасиво. Допустим есть блок подменю, несколько пунктов. Он появится, когда сработает родительский onmouseover()-обработчик. На этом подменю назначен обработчик onmouseout(), его скрывающий. При движении по пунктам подменю (на деле, HTML-элементам) на каждом из них возникнет событие mouseout (без обработки конечно), которое всплывет до назначенного на блок обработчика и скроет все подменю. После чего на очередном пункте с курсором возникает событие mouseover, которое всплывает до родителя блока с назначенным обработчиком. В итоге подменю снова появится, с выделенным под курсором пунктом. Получается мигание.

Вопрос на засыпку: как после скрытия блока, пункт меню в нем может получить mouseover, когда его не видно?! Я предполагаю, что события mouseout и mouseover возникают одновременно, т.е. когда один элемент теряет курсор, в тот же момент другой элемент его получает. Оба события регистрируются и обрабратываются по очереди.

Кстати, всплытие обоих событий не остановится на текущем родителе. Т.е. если мышь двигается где-нибудь на 4-5 уровне меню, вплытие событий уйдет до корневого пункта этой ветки меню, мигнув по дороге всеми блоками, и дальше вплоть до тега <html>.

Другая особенность кода: обход документа функцией getNodeIdx(). Зачем, спрашивается, лишний код, когда все элементы меню всегда на одном месте и к ним можно обратиться по индексу массива ChildNodes? Затем, что это были мои грабли %) Пустая строка с концевиком "\n" в исходном html-коде тоже считается за элемент массива ChildNodes, причем по-разному в разных браузерах. Так однажды влезешь в исходник, расставишь пару переносов, чтоб читалось удобнее, а у тебя уже меню не работает!

Вместо ChildNodes можно использовать массив children, там всегда элементы под одним индексом. Но я читал где-то, что это устаревший объект, к тому же привязка на индекс несколько ограничивает разработку. Дело вкуса.

Приведенный код скрипта - это обобщенный обработчик для вертикального и горизонтального меню. Если нужно только вертикальное, тогда первый блок с расчетом координат можно выпилить. Если меню нужно открывать в другую сторону, тогда подправить расчет координат и все :) Конечно при этом CSS и НТМL описания менюшек должны быть правильными.

[UPD] Была проблема дизайна: боковой блок меню перехватывал мышь при движении по пунктам верхнего меню, перекрывающим боковое. Все решилось изменением свойства z-index. Просто я не правильно понимал его обработку, потом вычитал в умной книге по JavaScript, что к чему, и исправил положение :)

[1oo%, EoF]

Понравилась статья? Расскажите о ней друзьям:


Комментарии
Для работы модуля комментариев включите javaScript


Показать/скрыть правила
Имя
[i] [b] [u] [s] [url]
:-) ;-) :D *lol* 8-) :-* :-| :-( *cry* :o :-? *unsure* *oops* :-x *shocked* *zzz* :P *evil*

Осталось 1000 символов.
Код защиты от спама Обновить код
Каждый комментарий проходит ручную модерацию. 100% фильтрация спама.
Продвижение
Время
Метки