Начало · Справочники · Курсы · Разговоры

leechy.ru · Сайт почти придуман

DOM: Нестандартный HTML

Очень часто, для работы клиентских скриптов нам нужны дополнительные данные, которых изначально нет на странице. Обычно они задаются как значения каких-то переменных прямо в JavaScript'е. Примером могут служить настройки крайне популярных HierMenus или мое дерево. На самом деле в этом нет абсолютно ничего плохого. Таким образом задаются дополнительные данные для практически всех скриптов, которые сложнее, чем роловер. Недостатком этого способа можно считать только плохая читабельность - посмотрев впервые на такой код далеко не каждый поймет что к чему относиться.

Более понятным для человека может являтся использование XML'ных конструкций. Помимо повышенной читабельности они дают возможность правильнее задавать древовидные структуры, которые используются в приведенных выше примеров. Подобные конструкции очень легко обрабатываются DOM-функциями, но недоступны для старых браузеров, которые не поддерживают DOM level 1. Т.е. ими можно пользоваться в настоящее время только в Netscape6/Mozilla и MSIE5+.

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

Нестандартные атрибуты

HTML - хороший язык. И наверное самое лучшее в нем - то, что программы его понимающие (браузеры) должны игнорировать все тэги и атрибуты, с которыми они не знакомы. На этом принципе построены почти все дополнения в нем - фреймы, стили и т.п. Т.е. браузер, который не понимает CSS просто проигнорирует атрибут style для элемента и никаких ошибок не выдасть - это не его дело, что там за атрибуты еще могут быть.

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

<img src="image.gif" name="myImage" defaultSrc="image.gif" selectedSrc="image-s.gif" activeSrc="image-a.gif" onmouseout="this.src = this.getAttribute('defaultSrc');" onmouseover="this.src = this.getAttribute('selectedSrc');" onmousedown="this.src = this.getAttribute('activeSrc');">

В данном примере используется DOM-метод getAttribute(), с чьей помощью нужно доставать значения как стандартных так и нестандартных атрибутов. Хотя, если использовать максимально дотошно методов DOM, то в обработчиках нужно было использовать также и setAttribute(), чтобы задать нового значения для src:

<img onmouseout="this.setAttribute('src', this.getAttribute('defaultSrc'));">

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

Конечно, атрибуты можно доставать не только из самого элемента. Метод getAttribute() есть для всех элементов страницы и для его использование достаточно просто обратиться к конкретному элементу. Например, чтобы сделать предварительную загрузку всех альтернативных изображений для роловеров можно просто "пробежаться" по всем тэгам img на странице и проверить есть ли у них соответствующие атрибуты. Это можно сделать с помощью коллекции document.images, но если в вашем скрипте вам будут нужны элементы, которые не попадают ни в одной стандартной коллекцией, то нужно воспользоваться методом getElementsByTagName():

var preloadedImages = new Array();
function preloadAlternateImages() {
   if (document.getElementsByTagName) {
      var allImages = document.getElementsByTagName('img');
      for (var i = 0; i < allImages.length; i++) {
         if (allImages.item(i).getAttribute('name')) {
            var imageName = allImages.item(i).getAttribute('name');
            if (allImages.item(i).getAttribute('defaultSrc')) {
               preloadedImages[imageName + 'defImg'] = new Image();
               preloadedImages[imageName + 'defImg'].src = allImages.item(i).getAttribute('defaultSrc');
            }
            // ... то-же для selectedSrc и activeSrc;
         }
      }
   }
}

Данная функция должна вызываться после загрузки документа, например на onload, потому что раньше просто не будут тэги <img>. Наверное заметили, что в функции проверяется наличие тэгов с помощью все того-же метода getAttribute(), вместо hasAttribute(). Дело в том, что hasAttribute() появился в DOM level 2 и работает только в Netscape6/Mozilla. MSIE6 поддерживает только DOM level 1. Второй способ определения атрибута - использовать свойство specified не очень удачен из-за того, что метод getAttributeNode(), с помощью которого нужно обращаться к атрибуту, чтобы проверить его свойства поддерживается только MSIE6 и Netscape6/Mozilla, а обращение к атрибуту как к свойству объекта (object.defaultSrc) - не является правильным и может не сработать, если касается нестандартных атрибутов.

И конечно, если будете подгружать картинки, нужно чуть-чуть изменить код обработчиков событий, в котором теперь будет еще и проверка на наличие подгруженного изображения:

<img onmouseout="if (preloadedImages[this.getAttribute('name') + 'defImg']) this.setAttribute('src', preloadedImages[this.getAttribute('name') + 'defImg']);">

Для полноты картины нужно отметить также, что методом setAttribute() можно устанавливать любые атрибуты, как стандартных, так и придуманных вами. А методы createAttribute() и setAttributeNode(), которые предназначены для этого, все еще недстаточно корректно обрабатываются браузерами.

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

Нестандартные тэги

HTML - хороший язык. Как я уже отмечал, не только все атрибуты, но и все элементы на странице, которые не понимаются браузерами будут проигнорированны. Только нужно помнить, что игнорируются только незнакомые тэги, а не все элементы. Т.е. если у вас внутри тэга есть текст или HTML-фрагмент - он покажется на странице... и это плохо. Это означает, что текст, который вам нужно разместить должен быть в атрибутах.

Несмотря на это, для цели, с которой я начал статью, нестандартные тэги очень удобны. Приведу в качестве примера часть структуры дерева этого сайта:

<tree>
   <branch id="item1" name="Как сделать?" uri="/howto/" isopen="false">
      <branch id="item2" name="Слои" uri="/howto/layers/" />
      <branch id="item3" name="Делаем дерево из таблицы" isopen="true">
         <branch id="item4" name="Часть 1" uri="/howto/tree1/" />
         <branch id="item5" name="Часть 2" uri="/howto/tree2/" />
      </branch>
      <branch id="item6" name="Играем в прятки" uri="/howto/hiding/" />
      <branch id="item7" name="Скажи мне, что у тебя за браузер..." uri="/howto/browserdetect/" />
   </branch>
   <branch id="item8" name="Общие" uri="/common/" isopen="false">
      <branch id="item9" name="Есть в браузере такая фича?" uri="/common/hasfeature/" />
      <branch id="item10" name="Netscape 6 вышел!" uri="/common/nn6out/" />
   </branch>
</tree>

Этот код не будет показан ни одним браузером, пока тэги <tree> и <branch> не станут стандартными. Несмотря на это, этот код доступен DOM-методам. К тому-же понять какие ветки являются дочерными по отношении какой, а также, где указывается имя, где путь и каким образом нужно добавлять новые ветки - просто прозрачно даже для пользователя впервые увидившего код.

Примечание

Примечание

К сожалению код, приведенный ниже будет работать только в NN6/Mozilla в случае, если он изначально присутствовал на странице. MS Internet Explorer распознает подобные нестандартные тэги в DOM-дереве документа, только если они были вставлены в дерево стандартными DOM-методами, описанными коротко в статье DOM: Коротко о Создании. Прошу прощения за эту ошибку с моей стороны. Тем не менее описанные DOM-методы работают для стандартных тэгов, для сгенеренного контента и для импортированного XML!

Для того, чтобы достать все данные и привести их к тому виду, который описан в статье "Делаем дерево из таблицы (часть 2)" придется снова воспользоваться методом getElementsByTagName(), но по самим веткам дерева будем бежать свойствами firstChild и nextSibling, чтобы показать как они работают:

function getBranches(parentItem, level) {
   if (parentItem.hasChildNodes()) {
      var parentId = (parentItem.getAttribute('id'))? parentItem.getAttribute('id') : '';
      var currentItem = parentItem.firstChild;
      while (currentItem) {
         if (currentItem.nodeType == 1 && currentItem.tagName == 'BRANCH') {
            var currentId = currentItem.getAttribute('id');
            var isOpenAttr = currentItem.getAttribute('isOpen');
            var isOpen = (isOpenAttr && isOpenAttr == 'true')? true : false;
            treeItems[currentId] = new treeItem(currentId, level, parentId, isOpen);
            getBranches(currentItem, level + 1);
         }
         currentItem = currentItem.nextSibling;
      }
   }
}
 
function getTrees() {
   var treeElement = document.getElementsByTagName('tree').item(0);
   if (treeElement) getBranches(treeElement, 0);
}

В самом начале функции getTrees() мы присваиваем treeElementdocument.getElementByTagName('tree').item(0). Т.е. первый элемент <tree>, который есть на странице. Просто предполагается, что он будет единственный. Если в вашем случае нужно использовать больше, чем один основной тэг, то тогда нужно обработать всех элементов таким-же образом, каким обрабатываются тэги <img> в примере с роловерами.

Если есть treeElement, то запускается функция getBranches(), которая собственно и будет доставать информацию. Ей передаем текущий родительский элемент (в начале это <tree>) и текущий уровень (это необходимо скрипту, из статьи). Начинаем с проверки есть ли дочерные элементы (в терминологии DOM их называют «нодами», от англ. node - узел, но «узлами» я называть их не буду) у родительского элемента. Для этого есть метод hasChildNodes(), который возвращает true если есть под-ноды. Проверка нужна, потому, что мы будем запускать функцию getBranches() для всех веток дерева, чтобы унифицировать код.

Первый дочерный элемент, в случае если есть под-ноды, указан в свойстве firstChild. Кроме этого у каждого нода есть также свойство lastChild, которое указывает соответственно последного под-нода. Но в данном случае нам нужно «бежать» по дереву сверху-вниз, поэтому использую firstChild.

Цикл, с помощью которого будем рассматривать всех под-нодов, это простейший while, в теле которого переменной currentItem присваивается currentItem.nextSibling. Свойство nextSibling - указатель на следующий нод этого уровня. Т.е. если currentItem.firstChild - это первый под-нод, то currentItem.nextSibling - следующий под-нод для родителя currentItem. Как наверное догадываетесь есть еще и previousSibling, который указывает соответственно на предыдущего нода в списке.

Итак, в чем состоит обработка всех под-нодов? Для начала нужно определить что это за нод. Ноды бывают разными. В спесификации их 9 видов, но поскольку MSIE5 различает всего два из них - тэги и текст, то пока не буду описывать всех. Теперь подробнее... у каждого нода есть свойство nodeType - это константа, чье числовое значение указывает что это за нод. У тэгов nodeType равен 1 (единице), у текста - 3 (тройке). На возникающий вопрос «Почему нужно проверять что за нод обрабатываем?» ответить просто - все пробелы и переносы строк, которые находятся между элементами и в HTML обычно игнорируются на самом деле являются текстовыми нодами. Т.е. между двумя нодами <branch> есть еще и один нод со значением nodeType = 3. Это очень часто не учитывается при написании скриптов и случаются ошибки.

Помимо проверки типа, нужно также проверить и имя тэга. Оно содержится в свойстве tagName. Проверка делается из-за того, что между ветками дерева можно вставлять любые другие тэги, даже HTML (которые отобразятся на странице), но нас интересуют только тэги <branch>.

Далее, нужно узнать информацию, которая нам нужна для того, чтобы сделать массив treeItems. А это в данном случае только id и isopen. Остальные данные сейчас не нужны. Может быть они понадобятся для построения самого дерева. Также нужен id родительского нода, его можно узнать всего один раз в самом начале обработки под-нодов, что и сделано. Думаю, не нужно разъяснят также и то каким образом достаем данные из других атрибутов.

В самом конце просто добавляем в массив treeItems новую запись и запускаем функцию getBranches() для поиска эвентуальных под-веток текущей. Вот и все.

Думаю все просто. Если вы разобрались, значить знаете всех основных DOM-методов и свойств с помощью которых можете работать не только над нестандартными HTML-элементами, но также и с самым-обычным HTML-кодом. Для справки приведу их список:

getElementById('id-тэга')
возвращает указатель на элемент с уникальным атрибутом id
getElementsByTagName('имя-тэга')
возвращает масив из элементов с заданным именем
getAttribute('имя-атрибута')
возвращает значение указанного атрибута
setAttribute('имя-атрибута', 'значение-атрибута')
задает новый атрибут или меняет значение старого
hasChildNodes()
возвращает ^macro[tt;true] если у этого нода есть дочерние ноды, в противном случае - false
firstChild
указывает на первый дочерный нод
lastChild
указывает на последний дочерный нод
nextSibling
указывает на следующий нод
previousSibling
указывает на предыдущий нод
nodeType
возвращает значение типа
tagName
возвращает имя тэга (для тэгов, если это текстовой нод, то возвращает '#text')