четверг, января 17, 2008

Изобретаем велосипед, то есть динамическое меню

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

Через два часа поисков я понял, что изобретать придётся. Варианты делились на две большие группы: кустарные (то есть ограниченно работоспособные) и маниакально-величественные (требовавшие установки библиотек, инициализации приложений и танцев с бубнами).

В моём распоряжении была библиотека Prototype, успевшая обновиться до версии 1.6.

Стратегия решения: каждый пункт меню являет собой div с атрибутом float: left. Открываемое подменю — тоже div, но абсолютно позиционированный так, чтобы возникнуть под своим родительским пунктом.

Дополнительные условия: 1) меню описано как массив в PHP для простоты работы; 2) позиционирование подменю не задаётся во время дизайна, а определяется при загрузке страницы. Иначе нельзя избежать мелких косяков из-за особенностей рендеринга в разных браузерах, а изменение строки меню вообще выльется в хаос.

Среди новых методов Element в этой версии Prototype обнаружил два просто замечательных:

absolutize
1.6
Turns element into an absolutely-positioned element without changing its position in the page layout.

clonePosition
1.6
Clones the position and/or dimensions of source onto element as defined by the optional argument options.

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

Создание HTML-кода меню в PHP:

$x=1;
foreach ($MENU as $d=>$m) {
if (is_array($m)) {
echo "<div id='item$x' class='menuItem' onClick='menuExpand($x);'>$d</div>";
echo "<div id='submenu$x' class='subMenu' style='display: none;'>";
foreach ($m as $item=>$href) {
echo "<nobr><a href='$href' class='menuItemHref'>$item</a></nobr><br />";
}
echo "</div>";
?>
<script type='text/javascript'>
Menu[Menu.length]=<?php echo $x; ?>
</script>
<?php
}
else {
echo "<div id='item$x' class='menuItem'><a href='$m' class='menuItemHref'>$d</a></div>";
}
++$x;
}

Понятно, что, если какое-то значение в массиве $MENU — тоже массив, то это описание подменю; иначе пункт меню — сам по себе ссылка, без наворотов.

Яваскрипт, добавляющий текущий индекс родительских пунктов меню в глобальный явскрипт-массив Menu, нужен будет для инициализации подменюшек.

Сперва я сделал просто: сразу после создания div для подменю инициализировал его позицию. Это работало в Firefox, но в IE не прошло из-за чудной манеры последнего создавать DOM лишь по окончании загрузки страницы, то есть когда я пытался получить свойства div#item1, его ещё не существовало в IE.

Пришлось просто заносить в массив Menu те пункты, у которых есть подменю, чтобы инициализировать их позже, по событию onLoad:

function menuInit() {
var menuHeight=$('item1').getHeight();
Menu.each(function(expandThis) {
$('submenu'+expandThis).absolutize();
$('submenu'+expandThis).clonePosition('item'+expandThis, { setHeight: false, offsetTop: menuHeight });
});
}

Мы берём высоту меню в переменную menuHeight, затем проходимся по всем подменю, делая их позицию абсолютной и помещая их относительно родительского пункта, смещая вниз на высоту оного.

Теперь прикручиваем обработчик нажатия на родительское меню:

function menuExpand(n) {
Menu.each(function(i) {
// Закрыть возможные открытые подменю, тупо пряча их все
if (i!=n) { $('submenu'+i).hide(); }
});
$('submenu'+n).toggle();
}

Метод toggle здесь вместо ожидаемого show для того, чтобы повторное нажатие на родительский пункт закрывало подменю.

Описываем меню в CSS:

.menuItem {
float : left;
padding-left: 7px;
padding-right: 20px;
font-family: sans-serif;
font-size: 10pt;
cursor: pointer;
}

.menuItemHref {
color: black;
text-decoration: none;
}

.subMenu {
display: table;
border: solid 1px black;
padding: 5px;
font-family: sans-serif;
font-size: 9pt;
background-color: #e98080;
line-height: 15pt;
}

Собираем в кучу, и... мама, мама, что делается, Люська с Манохиным прилетели...



Всё меню создано десятком строк на Яваскрипте, совместимо с тремя основными браузерами (возможно, и с остальными). Prototype — это страшная сила.

2 комментария:

eGlyph комментирует...

Привет. Художник ты авторитетный, спору нет. А насчет РНР разреши дать небольшой совет: не выводи HTML в PHP коде. Есть шаблоны, это окупается сторицей.

Сергей комментирует...

Я вот когда соображаю, на сколько вещь надо делать правильно, а на сколько
— эффективно, вспоминаю пример, читанный в книге по Перлу. Там високосный год вычислялся регистровым сдвигом, то есть, несмотря на уровень языка — практически средствами ассемблера. В смысле перспектив рефакторинга этот код был чудовищен, конечно. Лично я 15 минут чёркал по листочку карандашом, чтобы понять, как он работает.
Но зато как он работал!

Правильно было бы, конечно, использовать подходящий модуль. Только что правильнее — решить задачу правильным способом или решить её оптимально? Правильного ответа нет, мне кажется. Надо балансировать.

Поиск по этому блогу