К 2001 году JavaScript стал достаточно мощным, чтобы в реальном времени менять HTML-элементы - их стили, позиции и видимость, без перезагрузки страницы. Технология получила название DHTML, или Dynamic HTML. Это был не новый язык и не спецификация - это был маркетинговый термин для комбинации JavaScript, CSS и Document Object Model, используемой для создания интерактивных эффектов.
Главное применение DHTML в 2001 году - выпадающее навигационное меню. Каждый корпоративный сайт хотел такое. Наведи на «Продукты» - появляется подменю с подкатегориями. Наведи на «Услуги» - другое подменю. Без перезагрузки. Без Flash. Чистый HTML, CSS и JavaScript.
Заставить это работать одновременно в Internet Explorer 5 и Netscape 6 - настоящий инженерный вызов.
Проблема совместимости браузеров
IE5 и Netscape 6 оба поддерживали W3C DOM (document.getElementById, element.style), но расходились в модели событий. Netscape 4 с его document.layers мы игнорируем - к 2001 году его доля упала настолько, что это считалось допустимым ограничением.
// IE5: onmouseover/onmouseout срабатывали на элементе И на всех дочерних
// Netscape 6: та же логика - W3C-модель всплытия
// Но свойства объекта события различались:
// IE5:
element.onmouseover = function() {
var target = window.event.srcElement; // только IE
var relatedTarget = window.event.fromElement;
};
// Netscape 6 / W3C:
element.onmouseover = function(e) {
var target = e.target;
var relatedTarget = e.relatedTarget;
};
// Кросс-браузерные обёртки - были у всех:
function getEvent(e) { return e || window.event; }
function getTarget(e) { return e.target || e.srcElement; }
function getRelatedTarget(e) { return e.relatedTarget || e.fromElement; }
Полная реализация выпадающего меню
Подход: абсолютно позиционированные <div> скрыты по умолчанию, показываются при onmouseover, скрываются при onmouseout. Сложность: при движении мыши из родительской ссылки в подменю срабатывал onmouseout на родителе и закрывал меню до того, как пользователь успевал кликнуть.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>DHTML-навигация - 2001</title>
<style type="text/css">
body, td, th { font-family: Arial, Helvetica, sans-serif; font-size: 13px; }
#navbar {
width: 100%;
background-color: #336699;
height: 28px;
position: relative;
}
.nav-item {
position: relative;
float: left;
}
.nav-item a {
display: block;
float: left;
padding: 5px 14px;
color: #ffffff;
text-decoration: none;
font-weight: bold;
font-size: 12px;
}
.nav-item a:hover { background-color: #4477aa; }
.submenu {
position: absolute;
top: 28px;
left: 0;
width: 170px;
background-color: #f5f5f5;
border: 1px solid #336699;
border-top: 2px solid #336699;
display: none;
z-index: 100;
}
.submenu a {
display: block;
padding: 5px 10px;
color: #333333;
text-decoration: none;
border-bottom: 1px solid #dddddd;
font-weight: normal;
}
.submenu a:hover {
background-color: #d0e0f0;
color: #003366;
}
</style>
</head>
<body>
<div id="navbar">
<div class="nav-item" id="nav-products">
<a href="products.html"
onmouseover="showMenu('menu-products')"
onmouseout="scheduleHide('menu-products')">
Продукты ▼
</a>
<div class="submenu" id="menu-products"
onmouseover="cancelHide('menu-products')"
onmouseout="scheduleHide('menu-products')">
<a href="products/software.html">Программное обеспечение</a>
<a href="products/hardware.html">Оборудование</a>
<a href="products/services.html">Сервис</a>
</div>
</div>
<div class="nav-item" id="nav-about">
<a href="about.html"
onmouseover="showMenu('menu-about')"
onmouseout="scheduleHide('menu-about')">
О компании ▼
</a>
<div class="submenu" id="menu-about"
onmouseover="cancelHide('menu-about')"
onmouseout="scheduleHide('menu-about')">
<a href="about/company.html">О нас</a>
<a href="about/team.html">Команда</a>
<a href="about/careers.html">Вакансии</a>
</div>
</div>
<div class="nav-item" id="nav-contact">
<a href="contact.html"
onmouseover="showMenu('menu-contact')"
onmouseout="scheduleHide('menu-contact')">
Контакты ▼
</a>
<div class="submenu" id="menu-contact"
onmouseover="cancelHide('menu-contact')"
onmouseout="scheduleHide('menu-contact')">
<a href="contact/office.html">Наш офис</a>
<a href="contact/form.html">Написать нам</a>
</div>
</div>
</div>
<script type="text/javascript">
// DHTML-меню - стиль 2001 года
// Требования: IE5+, Netscape 6+, Opera 5+
var hideTimers = {};
function showMenu(menuId) {
hideAllMenus();
var menu = document.getElementById(menuId);
if (menu) {
menu.style.display = 'block';
}
cancelHide(menuId);
}
function scheduleHide(menuId) {
if (hideTimers[menuId]) {
clearTimeout(hideTimers[menuId]);
}
// 300 мс - достаточно, чтобы переместить мышь из ссылки в подменю
hideTimers[menuId] = setTimeout(function() {
hideMenu(menuId);
}, 300);
}
function cancelHide(menuId) {
if (hideTimers[menuId]) {
clearTimeout(hideTimers[menuId]);
hideTimers[menuId] = null;
}
}
function hideMenu(menuId) {
var menu = document.getElementById(menuId);
if (menu) { menu.style.display = 'none'; }
hideTimers[menuId] = null;
}
function hideAllMenus() {
var divs = document.getElementsByTagName('div');
for (var i = 0; i < divs.length; i++) {
if (divs[i].className === 'submenu') {
divs[i].style.display = 'none';
}
}
}
document.onclick = function() { hideAllMenus(); };
</script>
</body>
</html>
Трюк с setTimeout
Самая важная техника в этой реализации - задержка скрытия на 300 мс. Без неё:
- Пользователь наводит на «Продукты» → подменю появляется
- Пользователь ведёт мышь от ссылки к подменю
- Срабатывает
onmouseoutна ссылке немедленно - Подменю исчезает раньше, чем мышь туда добралась
Решение: не скрывать сразу. Запустить setTimeout. Когда мышь входит в подменю - вызвать cancelHide, обнулив таймер. Подменю остаётся открытым, пока мышь где-то внутри.
// Почему именно 300 мс? Эмпирическое тестирование в 2001 году:
// 100 мс - слишком быстро, медленные мыши и трекболы не успевали
// 500 мс - меню "прилипало", не закрывалось когда надо
// 300 мс - золотая середина на железе 2001 года
// Современные библиотеки тултипов и дропдаунов используют тот же диапазон.
Война z-index: проблема с select в IE5
В 2001 году z-index работал непредсказуемо в разных браузерах. У IE5 был печально известный баг: элементы <select> (выпадающие списки HTML) всегда отображались поверх абсолютно позиционированных элементов, независимо от z-index. Ваше красивое DHTML-меню уходило под <select> на странице.
Решение: разместить <iframe> позади меню, размером как меню. Iframe создавал новый stacking context, перекрывая <select>.
// IE5/IE6 "iframe shim" - обязателен на страницах с <select> рядом с навбаром
function createIframeShim(menuElement) {
var shim = document.createElement('iframe');
shim.src = 'about:blank';
shim.style.position = 'absolute';
shim.style.border = '0';
shim.style.filter = 'alpha(opacity=0)'; // прозрачность только для IE
shim.style.zIndex = '99';
shim.style.width = menuElement.offsetWidth + 'px';
shim.style.height = menuElement.offsetHeight + 'px';
menuElement.parentNode.insertBefore(shim, menuElement);
menuElement.style.zIndex = '100';
return shim;
}
Такова была реальность DHTML-разработки в 2001 году. За каждым изящным приёмом прятался IE-специфичный костыль.
Счётчик посещений через document.write
Ещё одна DHTML-техника того времени - «живой» счётчик посетителей без перезагрузки. Использовался document.write() прямо в HTML - предшественник Ajax, не менее примитивный:
<!-- Загрузка счётчика из CGI-скрипта -->
<!-- Скрипт выводит JavaScript, вызывающий document.write() -->
<script type="text/javascript" src="/cgi-bin/counter.pl?output=js"></script>
<!-- counter.pl выводит примерно такое: -->
<!-- Content-Type: text/javascript -->
<!-- document.write("Вы посетитель номер <b>14 847</b>"); -->
Загрузка тега <script> с внешнего URL, выводящего JavaScript - именно так работали счётчики посещений, рекламные сети, веб-ринги и сторонняя аналитика. Паттерн из 2001 года стал основой всей индустрии рекламных технологий.
Чем стал DHTML
К 2005 году jQuery абстрагировал несовместимости браузеров, делавшие DHTML мучением. Подход с setTimeout для предотвращения преждевременного закрытия меню используется в каждой современной библиотеке тултипов и дропдаунов. Проблема z-index со <select> исчезла с улучшением CSS-спецификаций в браузерах примерно в 2007 году.
«Iframe shim» для перекрытия <select> ушёл в прошлое, когда IE7 исправил этот баг в 2006 году. Паттерн if (window.event) vs if (e.target) исчез, когда IE9 принял W3C-модель событий в 2011 году.
Что осталось: концептуальная модель - JavaScript манипулирует CSS-свойствами и видимостью элементов для создания интерактивного поведения без обращений к серверу. Именно эту модель реализует каждый современный UI-фреймворк - React, Vue, Angular. Выпадающее меню 2001 года и React Popover сегодня архитектурно одно и то же, разделённые двадцатью годами инструментального прогресса.