Итак, название данной статьи говорит само за себя. Задача заключается в
следующем:
- Требуется построить «дерево», на основе файла data.xml, где «Item» — узел
дерева;
- Данные необходимо получать получать при помощи AJAX;
- Должна быть возможность сворачивать/показывать узлы дерева при клике на
–/+;
- При клике на узел, должен появляться небольшой блок с содержимым
«Description», следующий клик по странице должен скрывать этот
блок.
Недолго обдумав пути решения проблемы, первое что
приходит на ум: получаем содержимое XML-файла методом GET при помощи объекта
XMLHttpRequest, тем самым получаем объект DOM XML-файла, доступ к которому будет
через свойство «responseXML». После этого, мы можем «общаться» с загруженным
XML-документом используя все возможности DOM в Javascript. Также стоит отметить
одну проблему в Internet Explorer (6,7?) по этому поводу: чтобы объект
XMLHttpRequest (ActiveXObject('MSXML2.XMLHTTP.3.0')) получил корректный
DOM-объект XML-файла, нужно запускать его через веб-сервер (т.е по адресу,
например, http://myhost/file.html, а не file://localhost/c:/file.html — как при
открытии файла, посредством файловой системы). Про работу с аяксом я уже писал
вот в этой
статье.
Получив необходимый DOM-объект XML-файла, дальше мы вправе
делать с ним что угодно. Я решил написать рекурсивную функцию для парсинга всего
дерева документа и перевода его в HTML-формат (т.к простая вставка XML-тегов в
HTML-документ была бы не корректной и отображалась бы везде по-разному,
независимо от того, какие CSS-стили применены к XML-тегам). Алгоритм был
следующий: рекурсивно пройтись по каждому элементу XML-дерева, и вставить его
содержимое в указанный HTML-тег. Для дополнительного удобства обработки дерева,
вторым аргументом я передаю JSON-объект, который содержит информацию о том, на
какие HTML-теги делать замену, какие аттрибуты XML-тегов, заменять на
соответствующие атрибуты HTML-тегов. Также имеется возможность добавления
дополнительных аттрибутов (например «class» для CSS).
Главным «подводным
камнем», с которым я столкнулся при написании рекурсивной функции на JS был тот
факт, что если мы объявляем новую переменную внутри функции без использования
ключевого слова «var», она добавляется в глобальную область видимости,
происходит «замыкание», и в итоге каша, бесконечные циклы и не работающий
скрипт. При объявлении переменных, перед которыми стоит ключевое слово «var» —
они будут доступны только в той области, в которой они были
объявлены.
Также, для более удобного использования скрипта, все 3 функции
(получение файла аяксом, парсинг XML DOM-дерева, обработка полученного
HTML-дерева), я собрал в один объект в глобальной области видимости. Что
получилось в результате смотрим ниже:
Файл
«xmltools.js»:
MyTools =
{
'ajax': function()
{
var form, handler, method, xhr, data;
form = arguments[0];
handler = arguments[1];
method = (form.method || arguments[2] || 'GET').toUpperCase();
try {
// create
xhr = window.XMLHttpRequest ? new XMLHttpRequest() : ( window.ActiveXObject ? new ActiveXObject('MSXML2.XMLHTTP.3.0') : null );
// prepare
datasend = '';
data = form.elements;
for(i = 0; i < data.length; i++) datasend += (data[i].name +'='+ encodeURIComponent( data[i].value ) + '&');
// processing
request = form.action;
xhr.open( method, request + ( method == 'GET' ? (request.indexOf('?') == -1 ? '?' : '&')+ Math.random()*10E25 : '' ), true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function() { if(xhr.readyState == 4 && handler != null) handler( xhr ) }
xhr.send( datasend );
}
catch(e) { alert( e.message ) }
},
'parseXMLTree': function( node, replacement )
{
var placeholder = node.hasChildNodes() ? document.createElement('span') : document.createTextNode('');
var container;
var childs, child;
var nodeName, nodeText, nodeAttr, tagName, tagAttr = {}, tagAttrAddon = {};
childs = node.childNodes;
for( var i = 0; i < childs.length; i++ )
{
child = childs[i];
nodeName = child.nodeName;
nodeAttr = child.attributes;
nodeText = child.textContent || child.text;
tagName = replacement[ nodeName ]
if( typeof tagName == 'object' )
{
tagAttr = tagName[1];
tagAttrAddon = tagName[2] || tagAttrAddon;
tagName = tagName[0];
}
if( nodeName == '#text' ) container = document.createTextNode(nodeText);
else {
container = document.createElement( tagName ? tagName : nodeName );
// sets xml-attributes
for( var a = 0; a < nodeAttr.length; a++ )
{
var attr = nodeAttr[a];
var attrName = tagAttr[ attr.nodeName ] || attr.nodeName;
container.setAttribute( attrName, attr.nodeValue );
}
// sets addon attributes
for( var b in tagAttrAddon) if( typeof tagAttrAddon[b] == 'string' ) container.setAttribute( b, tagAttrAddon[b] );
container.appendChild( arguments.callee(child, replacement) );
}
placeholder.appendChild( container );
}
return placeholder;
},
'getXMLTree': function( obj, container, getsource )
{
if( !obj.parsed )
{
var myself = this;
var container = typeof container == 'object' ? container : document.getElementById( container );
obj.innerHTML = 'Загрузка файла..';
this.ajax({'action': obj.href, 'elements': []},
function( xhr )
{
// получаем распарсенный в HTML DOM-объект XML-файла
var tree = myself.parseXMLTree( xhr.responseXML.getElementsByTagName('Items')[0], {
'Item' : ['dl', {'Id': 'title'}, {'class': 'item'}],
'Name' : 'dt',
'Description' : 'dd',
'Childs' : ['div', {}, {'class': 'subtree'}]
});
var items = tree.getElementsByTagName('dl');
var descs = tree.getElementsByTagName('dd');
var clear = function(){ for( var i = 0; i < descs.length; i++ ) if( descs[i].nodeName ) descs[i].style.display = 'none' }
// назначаем обработчики событий для элентов дерева
for( i = 0; i < items.length; i++ )
{
var child = items[i];
var childs = child.getElementsByTagName('div')[0];
child.getElementsByTagName('dt')[0].onclick = function( evt )
{
evt = evt || window.event;
evt.cancelBubble = true;
clear();
this.parentNode.getElementsByTagName('dd')[0].style.display = 'block'
}
var opener = document.createElement('i');
opener.innerHTML = childs.innerHTML ? '+' : '';
opener.className = childs.innerHTML ? 'opener' : 'disabled';
opener.onclick = function()
{
if( this.className == 'opener' )
{
var hidden = this.parentNode.getElementsByTagName('div')[0];
this.innerHTML = this.innerHTML == '+' ? '–' : '+';
hidden.style.display = !hidden.style.display || hidden.style.display == 'none' ? 'block' : 'none';
}
}
child.insertBefore( opener, child.firstChild );
}
// результирующая вставка обработанного «дерева» на страницу
container.appendChild( tree );
window.onclick = clear;
obj.innerHTML = 'XML-файл успешно загружен';
obj.className = 'disabled';
obj.parsed = true;
// просмотр полученного HTML-кода из XML-файла
document.getElementById( getsource ).innerHTML =
tree.innerHTML.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\n/g, '<br>')
.replace(/\t/g, ' ');
}
);
}
return false;
}
}
Стилевое оформление в CSS, под указанные HTML-теги и классы для
полученного дерева, получилось таким:
Файл
«style.css»:
#tree { padding:10px 0; width:50%; }
#tree .item { position:relative; display:block; float:left; clear:left; margin:5px; }
#tree .item dt { float:left; color:gray; border-bottom:1px dashed gray; cursor:pointer; }
#tree .item dt:hover { color:black; }
#tree .item dd { background:white; border:1px solid silver; padding:10px; width:250px; position:absolute; top:0; right:-290px; display:none; z-index:10; }
#tree .item .subtree { display:none; padding-left:30px; }
#tree .item .opener,
#tree .item .disabled { display:block; float:left; width:20px; height:20px; text-align:center; line-height:20px; cursor:pointer; margin-right:2px; }
#tree .item .disabled { background:none; cursor:default; }
#tree .item .opener:hover { color:green; background:#eee; }
Единственный
косяк, который мне пока не удалось побороть, как обычно выдал это сраный
наш «всеми любимый» IE 6 (возможно и 7). После получения и обработки документа,
вставка ноды (appendChild) происходит без влияния указанных в CSS-стилей. И вот
хоть убей его. А если производить вставку распарсенного HTML-дерева через
свойство «innerHTML», то CSS-стили применяются, но т.к мы делаем вставку копии
текста ноды, а не её саму, то все обработчики событий в JS, которые были указаны
в скрипте, конечно же не работают. Если кто разберется в этой проблеме, буду
благодарен, если вы отпишите по этому поводу в комментах к статье.
|