Практические советы по использованию DHTML

Самая главная причина медленного внедрения новой технологии заключается в том, что компании Microsoft и Netscape в погоне за «зелеными рублями» совсем не подумали о том, что в этой области без договоренности не обойтись, что, как бы они этого ни хотели, никто совместимостью жертвовать не будет. Вторая основная причина заключается в том, что аппаратно-программный комплекс пользователей пока по производительности не дотягивает до потребностей прожорливого DHTML. Страницы закачиваются исключительно медленно, динамические эффекты на странице реализуются рывками, не синхронно или не так быстро, как этого хотелось бы.

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

Несмотря на то, что доля четвертых версий популярных броузеров растет, рано еще забывать про старые версии, а также сбрасывать со счетов броузеры других производителей. Поэтому, планируя реализовать что-либо на своем сайте с помощью DHTML, прежде всего стоит задуматься о совместимости. Тут есть два пути. Первый путь возможен, когда реализуется нечто, без чего можно обойтись, например выскакивающие подсказки при наведении указателя на пункты меню. В этом случае код оформляется так, чтобы «старые» броузеры просто не замечали «лишнего». Второй путь возможен, когда требуется реализовать что-то важное, например средства навигации. В этом случае не обойтись без динамических подстановок. Оптимальным вариантом является совмещение DHTML с какой-либо серверной технологией — SSI, PHP, ASP. В этом случае сервер перед выдачей HTML-страницы посетителю формирует ее так, чтобы она содержала либо не содержала DHTML-элементы, в зависимости от броузера клиента.

Помимо совместимости по версиям приходится задумываться о совместимости по производителю. На начальном этапе изучения DHTML кажется, что различия в реализации DHTML от Microsoft и от Netscape невелики, но чем глубже погружаешься в проблему, тем больше находишь несоответствий. Поэтому не всегда нужно писать универсальный код, иногда легче, а порой это единственный способ, — написать полностью независимые реализации, подставляя нужную в зависимости от броузера.

Понятие слоя

Пора наконец объяснить, что же такое DHTML. Это нечто иное, как четвертый HTML с расширенным JavaScript. Вот от него-то все проблемы и берутся. Если стандартный JavaScript от Microsoft реализовал в общем-то так же, как и Netscape, то в расширенном каждый из них пошел своим путем, а расплачиваться за это приходится нам, web-мастерам.

Ключевыми понятиями в DHTML являются слои и события. Слой — это некий прямоугольный элемент, содержащий в себе любую разметку HTML. Слоем может быть как простая строка текста, так и сложная форма, сверстанная в таблице. С помощью JavaScript можно манипулировать таким слоем — изменять его размеры, видимость, перемещать его, а также изменять его «высоту». В общем случае слой — это часть HTML-файла, выделенная тегом DIV, которому присвоен некий идентификатор (ID).

<div id=”navmenu”>
...
</div>

Но слой не станет слоем, если его не описать с помощью стилевых таблиц (CSS).

<style type=”text/css”>
#navmenu {POSITION: absolute; TOP:0; LEFT:0; 
	Z-INDEX: 100; VISIBILITY: hidden; WIDTH: 250px; 
	HEIGHT: 400px;}
...
</style>

С помощью CSS описываются следующие параметры слоя:

POSITION — определяет точку отсчета координат. Если он равен absolute, то координаты отсчитываются относительно верхнего левого угла документа. Если он равен relative, то координаты отсчитываются от верхнего левого угла слоя, включающего данный слой.

TOP — определяет координату Y верхнего левого угла слоя. Как я уже говорил, положение слоя можно будет впоследствии менять.

LEFT — определяет координату X верхнего левого угла слоя.

Z-INDEX — описывает «уровень» слоя. Дело в том, что слои могут частично или полностью перекрываться. В этом случае Z-INDEX определяет, какой слой будет «выше» другого. Все слои, которые в принципе могут перекрываться, должны иметь различный Z-INDEX.

VISIBILITY — описывает начальную видимость слоя. Слой может быть изначально виден (visible) или спрятан (hidden).

WIDTH — задает ширину слоя. При этом надо помнить, что слой не будет растягиваться при увеличении в размерах его содержимого, например при увеличении пользователем размера шрифта. Поэтому все, что не поместится, будет обрезано по краю слоя. В принципе, можно не указывать ширину слоя, но тогда в IE проявится один неприятный побочный эффект — ширина слоя будет принята равной ширине окна или фрейма. В результате, перекрыв собой другие элементы страницы, он не позволит выделить текст, нажать на ссылки или совершить другие мышиные действия над элементами в «нижележащих» слоях.

HEIGHT — задает высоту слоя, которая также должна быть достаточной, чтобы вместить все содержимое слоя.

То, что слои описываются в стилевых таблицах, вызывает еще один неприятный побочный эффект. Дело в том, что в NN 4.x при отключении в настройках JavaScript заодно, непонятно почему, отключается и поддержка CSS. В результате слои перестают быть слоями и становятся обычными блоками документа. Соответственно, все невидимые слои вдруг становятся видимыми, перестают позиционироваться и отображаются на странице просто в порядке их описания в файле. Есть два способа борьбы с этим: использовать для описания слоев собственное изобретение Netscape — layers или генерировать содержимое слоев динамически, тем же JavaScript.

Манипулирование слоем

Netscape и Microsoft предложили абсолютно разные модели доступа к объектам DHTML. Например, доступ к свойству, определяющему видимость слоя, реализован так:

в NN:

document.layers[“layername”].visibility=“visible”;

в IE:

document.all[“layername”].style.visibility= ”visible”;

или:

document.all.item(“layername”).style.visibility= ”visible”;

В том случае, когда логика работы с объектами у NN и у IE совпадает, можно написать универсальный код, используя функцию eval. Но для этого необходимо сначала определить производителя броузера, а потом инициализировать определенные модификаторы.

var layerRef=”null”, styleSwitch=”null”;
if (navigator.appName == “Netscape”) {
	layerRef=”document.layers”;
	styleSwitch=””;
}else{
	layerRef=”document.all”;
	styleSwitch=”.style”;
}

После этого можно манипулировать свойствами слоев, пользуясь функцией eval.

eval(layerRef+’[“layername”]’+styleSwitch+
	’.visibility=”visible”’);

Справедливости ради надо отметить, что объектная модель IE гораздо лучше продумана и в несколько раз более полная, чем объектная модель NN. К примеру, IE предоставляет возможность обратиться к любому свойству любого элемента страницы, а это позволяет создавать разнообразные эффекты.

Изменение видимости слоя

Самое простое, что можно сделать со слоем, — это его спрятать или, наоборот, показать. Осуществляется это изменением свойства visibility. Если значение этого свойства устанавливается в visible, то слой будет отображен (конечно, если он находится в пределах окна), а если значение установлено в hidden, то слой будет спрятан. Для удобства напишем две функции — скрывания и показывания слоя.

function hideLayer(layerName){
	eval(layerRef+’[“‘+layerName+’”]’+styleSwitch+
	’.visibility=”hidden”’);
}
function showLayer(layerName){
	eval(layerRef+’[“‘+layerName+’”]’+styleSwitch+
	’.visibility=”visible”’);
}

Зачем нужны эти функции? Читайте дальше.

Предзагрузка изображений

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

<style type=”text/css”>
#loading {POSITION: absolute; TOP:0; LEFT:0; 
	Z-INDEX: 200; WIDTH: 100%; HEIGHT: 100%;}
#mainbody {POSITION: absolute; TOP:50; LEFT:150;
	VISIBILITY: hidden; Z-INDEX: 400;}
</style>
...
<div id=”loading”>
<table width=”100%” height=”100%” border=0>
<tr><td align=”center” valign=”middle”>
Подождите пожалуйста...
</td></tr>
</table>
</div>
<div id=”mainbody”>
... куча картинок ...
</div>

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

<body onload=”hideLayer(‘loading’); showLayer(‘mainbody’);”>

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

Каталог продукции

Принцип построения каталога продукции мало чем отличается от предыдущего примера, только слоев здесь больше, а их видимость меняется не по внутреннему событию, а в результате действий пользователя. Дополним наш скрипт еще парой функций, которые будут показывать «следующий» слой и «предыдущий».

function showPreviousLayer(){
	var layerNumToShow=layerNumShowing-1;
	if (layerNumToShow < 1)
	{layerNumToShow=totalLayersInLoop;}
	hideLayer(eval(‘“layer’ + layerNumShowing+
	’”’));
	showLayer(eval(‘“layer’ + layerNumToShow+’”’));
	layerNumShowing=layerNumToShow;
}

function showNextLayer(){
	var layerNumToShow=layerNumShowing+1;
	if (layerNumToShow > totalLayersInLoop)
	{layerNumToShow=1;}
	hideLayer(eval(‘“layer’ + layerNumShowing
	+’”’));
	showLayer(eval(‘“layer’ + layerNumToShow+’”’));
	layerNumShowing=layerNumToShow;
}

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

var totalLayersInLoop=3;
var layerNumShowing=1;

Изменение положения слоя

Немного более сложной задачей является изменение положения. Как в NN, так и в IE у слоя существуют свойства left и top, которые определяют положение левого верхнего угла слоя относительно начала координат документа. Но проблема заключается в том, что броузеры интерпретируют этот параметр по-разному. Если NN хранит в них число пикселей, то IE хранит в них строки, содержащие как само значение параметра, так и единицу измерения (px, pt, cm, el и т. д). Работать с такой строкой очень сложно, поэтому IE предоставляет для определения положения слоя еще четыре свойства — posLeft, posTop, pixelLeft и pixelTop. Первые два параметра хранят числовые значения top и left, а pixelLeft и pixelTop хранят те же значения, но выраженные в пикселях. Таким образом, чтобы оперировать этими свойствами в обоих броузерах, надо создать модификаторы.

var leftRef = “null”, topRef = “null”;
if (navigator.appName == “Netscape”) {
	leftRef = “.left”;
	topRef = “.top”;
}else{
	leftRef = “.pixelLeft”;
	topRef = “.pixelTop”;
}

После этого можно читать или модифицировать это свойство, пользуясь функцией eval.

eval(layerRef+’[“‘+layerName+’”]’+styleSwitch+
topRef+’ = newTop’);

Напишем функцию, которая будет перемещать заданный слой в заданные координаты.

function moveLayer(layerName,newLeft,newTop){ 
	eval(layerRef+’[“‘+layerName+’”]’+styleSwitch+
	topRef+’ = newTop’);
	eval(layerRef+’[“‘+layerName+’”]’+styleSwitch+
	leftRef+’ = newLeft’);
}

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

function moveLayerLeft(layerName,leftstop){ 
	if (eval(layerRef+’[“‘+layerName+’”]’+
	styleSwitch+leftRef+’ > leftstop’)){
		currLeft=eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’-10’);
		eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’ = currLeft’);
		setTimeout(‘moveLayerLeft(“‘+layerName+’”,’+
		leftstop+’)’,1);
	} else {
		eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+’.left = leftstop’);
	}
}

Понятно, что таким образом можно перемещать слой не только по прямой, но и по сложным траекториям, задавая их какой-либо математической формулой.

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

Также важно знать, что NN перерисовывает документ сразу после события onLoad, поэтому перемещение по этому событию слоя вне область отсечения окна броузера не спасает от появления полос прокрутки. Единственное решение, которое я пока нашел, — задерживать инициализацию состояний с помощью функции setTimeout.

<script language=”JavaScript”>
...
function showMenu(){
	if (navigator.appName == “Netscape”) {
		setTimeout(“moveLayer(‘wc-menu’,-240,10); 
		showLayer(‘wc-menu’);”, 1000);
	} else {
		moveLayer(‘wc-menu’,-240,10);
		showLayer(‘wc-menu’);
	}
}
</script>
...
<body onload=”showMenu();”>

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

Выплывающее меню

Создадим меню, которое прячется вне поля видимости, а по желанию пользователя, например при нажатии на специальный язычок, выезжает из-за границы окна. При повторном нажатии на этот язычок оно будет уезжать обратно. Прежде всего определим две функции — плавного перемещения вправо и влево.

function moveLayerRight(layerName,leftstop){ 
	if (eval(layerRef+’[“‘+layerName+’”]’+
	styleSwitch+leftRef+’ < leftstop’)){
		currLeft=eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’+10’);
		eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’ = currLeft’);
		setTimeout(‘moveLayerRight(“‘+layerName+’”,’+
		leftstop+’)’,1);
	} else {
		eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’ = leftstop’);
	}
}

function moveLayerLeft(layerName,leftstop){ 
	if (eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’ > leftstop’)){
		currLeft=eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’-10’);
		eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+leftRef+’ = currLeft’);
		setTimeout(‘moveLayerLeft(“‘+layerName+’”,’+
		leftstop+’)’,1);
	} else {
		eval(layerRef+’[“‘+layerName+’”]’+
		styleSwitch+’.left = leftstop’);
	}
}

Шаг перемещения и величина задержки определяются экспериментально на машине среднего класса с учетом размера и сложности перемещаемого слоя. Слой должен перемещаться плавно, но и не слишком медленно.

У нашего меню есть только два состояния — выдвинутое и спрятанное. К тому же изменяет эти состояния одна и та же ссылка. Поэтому мы должны создать функцию, которая будет отслеживать эти состояния и определять, что нужно делать, выдвигать или задвигать.

function showhide(){ 
	if (state==1) {
		if (eval(layerRef+’[“wc-menu”]’+
			styleSwitch+leftRef+’ < 10’)){
			moveLayerRight(“wc-menu”,10);
		} else {
			moveLayerLeft(“wc-menu”,10);
		}
		state=0;
		return;
	}    
	if (state==0) {
		if (eval(layerRef+’[“wc-menu”]’+
			styleSwitch+leftRef+’ < -240’)){
			moveLayerRight(“wc-menu”,-240);
		} else {
			moveLayerLeft(“wc-menu”,-240);
		}
		state=1;
		return;
	}
}

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

Создадим еще функцию инициализации меню, которая будет вызываться по событию onLoad и предотвращать появление полосы прокрутки.

var state=1;
function showMenu(){
	if (navigator.appName == “Netscape”) {
		setTimeout(“moveLayer(‘wc-menu’,-240,10);
		showLayer(‘wc-menu’);”, 1000);
	} else {
		moveLayer(‘wc-menu’,-240,10);
		showLayer(‘wc-menu’);
	}
}

Если бы мы решили, что начальное положение меню должно быть выдвинутое, то эта функция нам не потребовалась.

Теперь дело остается за малым — следует создать сам слой, содержащий меню.

<style type=”text/css”>
#wc-menu {POSITION: absolute;  VISIBILITY: hidden;
	LEFT: 0; TOP: 10; WIDTH: 260; Z-INDEX: 5000; }
</style>
...
<body onload=”showMenu();”>
<div id=”wc-menu”>
<table cellspacing=0 cellpadding=0 border=0 width=260>
...
</table>
</div>
...

Написав этот пример, я вспомнил об одном предупреждении. Не давайте слоям названий типа: menu, body, default, status и т. п. Такие названия могут совпасть с названиями внутренних элементов броузера, и он начнет странным образом глючить или откажется выполнять сценарий без всяких причин.

Скроллинг без полос прокрутки

Одной из наиболее раздражающих особенностей фреймов, с эстетической точки зрения, является появление полос прокрутки посередине окна, когда содержимое фрейма не помещается в отведенное ему пространство. С помощью DHTML можно легко побороть это, сделав страницу более привлекательной.

Прежде всего создадим необходимый frameset.

<frameset cols=”120,*” frameborder=”0” framespacing=”0” border=”0”>
<frame src=”menu.html” border=”0” marginheight=”0” marginwidth=”0”
name=”menu” scrolling=”no”>
<frame src=”news.html” border=”0” marginheight=”0” marginwidth=”0”
name=”main” scrolling=”no”>
</frameset>

Обратите внимание, что оба фрейма также имеют атрибут scrolling=«no». Мы ведь не хотим неэстетичных полос прокрутки.

Теперь создадим документ для правого фрейма. При этом все его содержимое поместим в слой.

<html><body>
<div id=”pageLyr” style=”position: absolute; 
	left: 10; top: 10;
	z-index: 1; visibility: visible;”>
Очень
...
длинный
...
слой
</div>
</body></html>

А в левый слой поместим элементы управления прокруткой. Я их реализовал в графическом виде.

<html><body>
<img name=”arrow_img” src=”center.gif” usemap=”#arrow_map”
	width=”90” height=”66” border=”0”>
<map name=”arrow_map”>
<area shape=poly coords=”0,27,...,0,27” 
	href=”JavaScript:top.pageScroll(‘up’)”
	onmouseover=”top.arrowToggle(‘up’)” 
	onmouseout=”top.arrowToggle(‘center’)”>
<area shape=poly coords=”0,28,...,0,28”
	href=”JavaScript:top.pageScroll(‘top’)”
	onmouseover=”top.arrowToggle(‘center’)”
	onmouseout=”top.arrowToggle(‘center’)”>
<area shape=poly coords=”0,51,...,0,51”
	href=”JavaScript:top.pageScroll(‘down’)”
	onmouseover=”top.arrowToggle(‘down’)” 
	onmouseout=”top.arrowToggle(‘center’)”>
</map>
</body></html>

Меняя изображение при наведении на него указателя, можно сделать элемент управления динамическим. Реализуется это с помощью маленькой функции и, понятно, никакого отношения к скроллингу не имеет.

function arrowToggle(dir) {
	parent.menu.document.arrow_img.src = 
	dir+”.gif”;
}

А за собственно скроллинг отвечает всего одна маленькая функция.

function pageScroll(dir) {
	var pageTop = parseInt(eval(‘parent.main.’+
	layerRef+’[“pageLyr”]’+styleSwitch+topRef));
	if(dir == “down”) {
		eval(‘parent.main.’+layerRef+’[“pageLyr”]’+
		styleSwitch+topRef+’ = (pageTop-50)’);
	} else if(dir == “up”) {
		if(pageTop >= 10) {
		eval(‘parent.main.’+layerRef+’[“pageLyr”]’+
		styleSwitch+topRef+’ = 10’);
		} else {
		eval(‘parent.main.’+layerRef+’[“pageLyr”]’+
		styleSwitch+topRef+’ = (pageTop+50)’);
		}
	} else if(dir == “top”) {
		eval(‘parent.main.’+layerRef+’[“pageLyr”]’+
		styleSwitch+topRef+’ = 10’);
	}
}

Изменение области отсечения слоя

У каждого слоя есть прямоугольная область отсечения. Только та часть слоя, которая попадает внутрь этой области, является видимой. По умолчанию область отсечения равна прямоугольной области самого слоя, но, изменяя значения этого свойства, мы можем отображать слой частично или вообще его прятать. Как обычно, обращение к этим свойствам в IE и в NN происходит по-разному. Но на этот раз дело не ограничивается только разными именами свойств. В IE реализован уникальный, весьма загадочный способ присвоения значений этим свойствам.

Если в NN можно написать:

document.layers[layerName].clip.left = 0;
document.layers[layerName].clip.right = 250;
document.layers[layerName].clip.top = 0;
document.layers[layerName].clip.bottom = 150;
то в IE можно присвоить только прямоугольник целиком:
document.all[layerName].style.clip=
’rect(0 250 150 0)’;

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

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

origwidth=document.layers[layerName].clip.right;
origheight=document.layers[layerName].clip.bottom;

Как вы, наверно, уже догадались, одними модификаторами тут уже не обойтись. Для того чтобы написать кроссплатформенный код, работающий с областью отсечения, дополним нашу коллекцию модификаторов еще одним.

var clipSwitch=”null”;
if (navigator.appName == “Netscape”) {
	clipSwitch=”.clip.”;
}else{
	clipSwitch=”.clip”;
}

В отличие от предыдущих случаев, здесь модификатор IE не указывает на стандартное свойство объекта, а адресует созданную нами переменную.

eval(layerRef+’[“‘+layerName+’”]’+clipSwitch+
	’left = 10’);

После выполнения этой строки в NN левая граница области отсечения будет на 10 пикселей правее левого края слоя, а в IE у этого слоя будет просто создана новая переменная document.all[layerName].clipleft. Чтобы действительно изменить область отсечения, нужно присвоить новому свойству значение.

document.all[layerName].style.clip =
	“rect(0 100% 100% “ + 
	document.all[layerName].clipleft + “)”;

Очевидно, что при этом требуется четко указывать, какие команды каким броузером должны выполняться.

if (navigator.appName != “Netscape”) {
	document.all[layerName].style.clip =
		“rect(“ + document.all[layerName].cliptop 
		+ “ “ +
		document.all[layerName].clipright + “ “ +
		document.all[layerName].clipbottom + “ “ +
		document.all[layerName].clipleft + “)”;
}