DHTML-навигация в Бишкеке 2001 года: выпадающие меню на чистом JavaScript
К 2001 году в бишкекских веб-студиях началась конкуренция. Клиенты уже знали, что у конкурента сайт с «красивым меню, которое само открывается». DHTML-навигация стала маркером профессионализма.
Реализовать её самостоятельно, без Flash, без сторонних библиотек - исключительно на JavaScript и CSS - значило знать платформу на глубоком уровне. Статьи Дмитрия Котерова на javascript.ru, книга «Dynamic HTML» Дэнни Гудмана, эксперименты в IE5 и Netscape 6. Так это делалось в Бишкеке в 2001 году.
Полная реализация: HTML + CSS + JavaScript
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Навигация DHTML - Бишкек, 2001</title>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<style type="text/css">
* { margin:0; padding:0; }
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 13px;
background: #EEEEEE;
}
/* Навигационная полоса */
#topnav {
background-color: #003399;
height: 32px;
position: relative; /* обязательно для дочерних absolute */
width: 100%;
}
/* Один пункт меню верхнего уровня */
.ni {
position: relative;
float: left;
height: 32px;
}
.ni > a {
display: block;
line-height: 32px;
padding: 0 16px;
color: #FFFFFF;
text-decoration: none;
font-weight: bold;
font-size: 12px;
white-space: nowrap;
}
.ni > a:hover,
.ni > a.active { background-color: #0044CC; }
/* Выпадающее подменю */
.drop {
display: none; /* скрыто по умолчанию */
position: absolute;
top: 32px; /* высота navbar */
left: 0;
min-width: 175px;
background: #FFFFFF;
border: 1px solid #003399;
border-top: 3px solid #003399;
z-index: 500;
/* box-shadow в IE5 не работал - обходились без него */
}
.drop a {
display: block;
padding: 7px 12px;
color: #222222;
text-decoration: none;
font-size: 12px;
border-bottom: 1px dotted #DDDDDD;
}
.drop a:hover {
background: #E8EEFF;
color: #003399;
}
/* Основной контент под навбаром */
.page-body {
padding: 20px;
background: #FFFFFF;
min-height: 400px;
}
</style>
</head>
<body>
<div id="topnav">
<div class="ni" id="ni-company">
<a href="company.asp"
onmouseover="ndOpen('drop-company','ni-company')"
onmouseout="ndClose('drop-company')">
О компании ▼
</a>
<div class="drop" id="drop-company"
onmouseover="ndKeep('drop-company')"
onmouseout="ndClose('drop-company')">
<a href="company/about.asp">О нас</a>
<a href="company/history.asp">История</a>
<a href="company/team.asp">Команда</a>
<a href="company/licenses.asp">Лицензии</a>
</div>
</div>
<div class="ni" id="ni-products">
<a href="products.asp"
onmouseover="ndOpen('drop-products','ni-products')"
onmouseout="ndClose('drop-products')">
Продукция ▼
</a>
<div class="drop" id="drop-products"
onmouseover="ndKeep('drop-products')"
onmouseout="ndClose('drop-products')">
<a href="products/computers.asp">Компьютеры</a>
<a href="products/periphery.asp">Периферия</a>
<a href="products/network.asp">Сетевое оборудование</a>
<a href="products/software.asp">Программное обеспечение</a>
</div>
</div>
<div class="ni" id="ni-support">
<a href="support.asp"
onmouseover="ndOpen('drop-support','ni-support')"
onmouseout="ndClose('drop-support')">
Поддержка ▼
</a>
<div class="drop" id="drop-support"
onmouseover="ndKeep('drop-support')"
onmouseout="ndClose('drop-support')">
<a href="support/faq.asp">Часто задаваемые вопросы</a>
<a href="support/drivers.asp">Драйверы</a>
<a href="support/contacts.asp">Контакты поддержки</a>
</div>
</div>
<div class="ni" id="ni-contacts">
<a href="contacts.asp"
onmouseover="ndOpen('drop-contacts','ni-contacts')"
onmouseout="ndClose('drop-contacts')">
Контакты ▼
</a>
<div class="drop" id="drop-contacts"
onmouseover="ndKeep('drop-contacts')"
onmouseout="ndClose('drop-contacts')">
<a href="contacts/bishkek.asp">Бишкек (главный офис)</a>
<a href="contacts/osh.asp">Ош</a>
<a href="contacts/form.asp">Написать нам</a>
</div>
</div>
</div><!-- /topnav -->
<div class="page-body">
<h2>Добро пожаловать</h2>
<p>Наведите курсор на пункты меню.</p>
<br>
<!-- Этот select тестировал баг IE5 с z-index: -->
<select style="width:200px;">
<option>Выберите город</option>
<option>Бишкек</option>
<option>Ош</option>
<option>Джалал-Абад</option>
</select>
</div>
<script type="text/javascript">
// DHTML-меню, Бишкек 2001 год
// Совместимость: IE 5.0+, Netscape 6.1+
var _timers = {}; // таймеры закрытия
var _shims = {}; // iframe-шимы для IE5
// Открыть подменю
function ndOpen(dropId, navId) {
ndCloseAll();
var drop = document.getElementById(dropId);
if (!drop) return;
drop.style.display = 'block';
// Добавить класс active на ссылку в родителе
var ni = document.getElementById(navId);
if (ni) {
var a = ni.getElementsByTagName('a')[0];
if (a) a.className = 'active';
}
// IE5 z-index fix: iframe под меню, чтобы не скрывалось за <select>
ndCreateShim(drop, dropId);
ndCancelClose(dropId);
}
// Мышь вошла в подменю - не закрывать
function ndKeep(dropId) {
ndCancelClose(dropId);
}
// Запланировать закрытие
function ndClose(dropId) {
ndCancelClose(dropId);
_timers[dropId] = setTimeout(function() {
ndHide(dropId);
}, 300);
// 300 мс - проверено на офисных мышах в Бишкеке.
// На трекболах советских времён требовалось чуть больше.
}
function ndCancelClose(dropId) {
if (_timers[dropId]) {
clearTimeout(_timers[dropId]);
_timers[dropId] = null;
}
}
function ndHide(dropId) {
var drop = document.getElementById(dropId);
if (drop) drop.style.display = 'none';
// Убрать класс active со всех ссылок навбара
var nis = document.getElementById('topnav').getElementsByTagName('a');
for (var i = 0; i < nis.length; i++) {
if (nis[i].className === 'active') nis[i].className = '';
}
// Скрыть шим
if (_shims[dropId]) _shims[dropId].style.display = 'none';
_timers[dropId] = null;
}
function ndCloseAll() {
var drops = document.getElementById('topnav').getElementsByTagName('div');
for (var i = 0; i < drops.length; i++) {
if (drops[i].className === 'drop') {
drops[i].style.display = 'none';
}
}
// Снять все active-классы
var as = document.getElementById('topnav').getElementsByTagName('a');
for (var j = 0; j < as.length; j++) {
if (as[j].className === 'active') as[j].className = '';
}
}
// -------------------------------------------------------
// IE5 iframe shim - подменю иначе уходит ПОД <select>
// Netscape 6 это не нужно, поэтому проверяем document.all
function ndCreateShim(dropEl, dropId) {
// Шим нужен только для IE (document.all - IE-специфика)
if (!document.all) return;
var shimId = 'shim_' + dropId;
if (!_shims[dropId]) {
var shim = document.createElement('iframe');
shim.id = shimId;
shim.src = 'about:blank';
shim.scrolling = 'no';
shim.frameBorder = '0';
shim.style.position = 'absolute';
shim.style.border = 'none';
shim.style.zIndex = '499'; // на 1 ниже меню (500)
shim.style.left = dropEl.style.left || '0px';
shim.style.top = '32px';
shim.style.width = '175px';
shim.style.height = '200px'; // с запасом
// Прозрачность в IE через filter
shim.style.filter = 'alpha(opacity=0)';
dropEl.parentNode.insertBefore(shim, dropEl);
_shims[dropId] = shim;
} else {
_shims[dropId].style.display = 'block';
}
}
// Закрыть всё при клике вне меню
document.onclick = function(e) {
var t = e ? e.target : window.event.srcElement;
// Если клик не внутри #topnav - закрыть всё
var inNav = false;
while (t) {
if (t.id === 'topnav') { inNav = true; break; }
t = t.parentNode;
}
if (!inNav) ndCloseAll();
};
</script>
</body>
</html>
Типичные проблемы, с которыми сталкивались в Бишкеке
float + position в IE5. В IE5 дочерний position: absolute внутри float: left иногда смещался не туда. Лечилось добавлением position: relative на родительский элемент:
/* Без этого IE5 позиционировал .drop относительно body, а не .ni */
.ni { position: relative; } /* обязательно! */
Кириллица в onmouseover. Если кириллица в JavaScript-строках была в другой кодировке чем страница - в статусной строке браузера появлялись крокозябры:
<!-- Кодировка страницы и JS должна совпадать -->
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<script type="text/javascript">
// Эта строка должна быть в том же файле и той же кодировке:
var menuHint = 'Информация о компании'; // windows-1251 - работает
</script>
Внешний .js файл. В 2001 году часть разработчиков выносила JS в отдельный файл nav.js. Это создавало проблему кодировки в IE5: браузер мог неправильно определить кодировку JS-файла. Решение - либо держать JS прямо в HTML, либо указывать charset в теге script:
<script type="text/javascript" src="nav.js" charset="windows-1251"></script>
Наследие этих 300 миллисекунд
Задержка в 300 мс перед закрытием меню появилась в бишкекском коде в 2001 году как результат тестирования на реальных пользователях с офисными мышами и трекболами. Это не была выдумка - это была измеренная потребность.
Двадцать пять лет спустя библиотеки вроде Radix UI, Headless UI, Floating UI используют тот же диапазон: 150-300 мс. Потому что физиология движения руки не изменилась. Только мышь стала точнее, и нижнюю границу чуть сдвинули.
DHTML 2001 года - это React Popover 2026 года, разделённые инструментарием. Принцип один: показать элемент при входе курсора, дать достаточно времени добраться до него, скрыть при уходе.