Skrypt menu drzewiastego
Jak zrobić system menu oparty na strukturze drzewa z rozwijanymi gałęziami (folderami), w których znajdują się odsyłacze do dokumentów?
Wymagana wiedza
- Zagnieżdżanie wykazów <ul>...</ul>
- Podstawy odsyłaczy
- Podstawy stylów CSS (m.in. arkusze stylów i klasy selektorów)
Skrypt menu drzewiastego
Aby uzyskać menu drzewiaste, utwórz plik tree.css w tym samym katalogu co strona i zapisz w nim:
ul.tree {
display: block;
margin-left: 0;
padding-left: 0;
}
ul.tree ul {
display: block;
margin-left: 0;
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
}
ul.tree li {
display: block;
list-style-type: none;
padding-left: 20px;
background-image: url("document.gif");
background-position: left top;
background-repeat: no-repeat;
}
ul.tree li.closed {
background-image: url("closed.gif");
background-position: left top;
background-repeat: no-repeat;
}
ul.tree li.opened {
background-image: url("opened.gif");
background-position: left top;
background-repeat: no-repeat;
}
ul.tree li a {
font-size: 13px;
text-decoration: none;
cursor: pointer;
}
ul.tree li a.folder {
cursor: pointer;
}
ul.tree li a.active {
font-weight: bold;
}
ul.tree li a:hover {
text-decoration: underline;
}Dodatkowo w tym samym katalogu utwórz plik tree.js i zapisz w nim:
/**
* @author Sławomir Kokłowski {@link https://www.kurshtml.edu.pl}
* @copyright NIE usuwaj tego komentarza! (Do NOT remove this comment!)
*/
function Tree(id)
{
this.id = id;
var url = unescape(window.location.href.replace(/#.*/, ''));
var base = window.location.protocol + '//' + window.location.host + window.location.pathname.replace(/[^\/\\]+$/, '');
this.click = function ()
{
for (var i = 0, el_node; i < this.parentNode.childNodes.length; i++)
{
el_node = this.parentNode.childNodes.item(i)
if (el_node.nodeName.toLowerCase() == 'ul')
{
el_node.style.display = el_node.style.display == 'none' ? 'block' : 'none';
this.parentNode.className = this.parentNode.className.replace(/(^| +)(opened|closed)( +|$)/g, ' ') + ' ' + (el_node.style.display == 'none' ? 'closed' : 'opened');
return;
}
}
}
this.start = function (el)
{
for (var i = 0, el_node; i < el.childNodes.length; i++)
{
el_node = el.childNodes.item(i);
if (el_node.nodeName.toLowerCase() == 'a')
{
el_node.onclick = this.click;
for (var j = 0; j < el_node.parentNode.childNodes.length; j++)
{
if (el_node.parentNode.childNodes.item(j).nodeName.toLowerCase() == 'ul')
{
el_node.parentNode.className += ' closed';
el_node.className = (el_node.className ? el_node.className + ' ' : '') + 'folder';
break;
}
if (el_node.parentNode.childNodes.item(j).nodeName.toLowerCase() == 'li') break;
}
var active = el_node.href && unescape(el_node.href.replace(/#.*/, '')) == url;
if (!active)
{
var rel = el_node.getAttribute('rel');
if (rel)
{
var matches = (' ' + rel + ' ').match(/\s+Collection\(([^)]+)\)\s+/i);
if (matches)
{
matches = matches[1].split(',');
for (var k = 0, pos = -1; k < matches.length; k++)
{
if (matches[k].charAt(0) == '[' && (pos = matches[k].lastIndexOf(']')) > 0)
{
if (new RegExp(unescape(matches[k].substring(1, pos)), matches[k].substring(pos + 1)).test(url))
{
active = true;
break;
}
}
else
{
if (/^[\/\\]/.test(matches[k])) matches[k] = window.location.protocol + '//' + window.location.host + matches[k];
else if (!/^[a-z0-9]+:/i.test(matches[k])) matches[k] = base + matches[k];
if (unescape(matches[k].replace(/[\/\\]\.([\/\\])/g, '$1').replace(/[^\/\\]+[\/\\]\.\.[\/\\]/g, '').replace(/#.*/, '')) == url)
{
active = true;
break;
}
}
}
}
}
}
if (active)
{
el_node.className = 'active';
var el_parentNode = el_node;
do
{
el_parentNode = el_parentNode.parentNode;
if (el_parentNode.nodeName.toLowerCase() == 'ul')
{
el_parentNode.style.display = 'block';
if (document.getElementById(this.id) != el_parentNode) el_parentNode.parentNode.className = el_parentNode.parentNode.className.replace(/(^| +)(opened|closed)( +|$)/g, ' ') + ' opened';
}
}
while (document.getElementById(this.id) != el_parentNode)
}
}
else if (el_node.nodeName.toLowerCase() == 'ul') el_node.style.display = 'none';
this.start(el_node);
}
}
if (document.getElementById && document.childNodes) this.start(document.getElementById(this.id));
}Następnie wklej w treści nagłówkowej strony <head>...</head> następujący kod:
<link rel="stylesheet" href="tree.css"> <script src="tree.js"></script>
Na koniec w wybranym miejscu strony - tam, gdzie ma się wyświetlać menu drzewiaste - wstaw kod oparty na technice zagnieżdżania wykazów (tylko wypunktowanie, czyli lista nieuporządkowana <ul>...</ul>). Sposób zagnieżdżania kolejnych punktów listy będzie automatycznie odzwierciedlał strukturę drzewa menu. Oto przykładowy kod:
<ul id="tree0" class="tree"> <li><a href="...">Dokument 1</a></li> <li><a>Folder 2</a> <ul> <li><a href="...">Dokument 2.1</a></li> <li><a href="...">Dokument 2.2</a></li> </ul> </li> <li><a>Folder 3</a> <ul> <li><a href="...">Dokument 3.1</a></li> <li><a>Folder 3.2</a> <ul> <li><a href="...">Dokument 3.2.1</a></li> <li><a href="...">Dokument 3.2.2</a></li> <li><a href="...">Dokument 3.2.3</a></li> </ul> </li> <li><a href="...">Dokument 3.3</a></li> </ul> </li> <li><a href="...">Dokument 4</a></li> </ul> <script> new Tree("tree0"); </script>
Rzeczy, które trzeba tutaj podkreślić:
- Najbardziej zewnętrzny znacznik
<ul>...</ul>powinien mieć przypisaną klasę CSS:class="tree". Dzięki temu punkty menu przyjmą odpowiedni wygląd. - Ten sam najbardziej zewnętrzny znacznik
<ul>...</ul>musi posiadać atrybut:id="tree0" - Zaraz poniżej kodu menu musi się znajdować blok
<script>...</script>. Zauważ, że wyróżniony w nim identyfikator tree0 musi być identyczny z tym, co wpisano jako wartość atrybutuid="..."na początku menu!
UWAGA!
Wewnątrz wszystkich znaczników <li>...</li> muszą znajdować się znaczniki odsyłaczy, czyli: <a>...</a>.
Odsyłacze znajdujące się w węzłach menu (czyli tworzące otwierane i zamykane foldery) nie powinny posiadać atrybutu href="...". Alternatywnie można wpisać tam href="javascript:void(0)".
W jednym dokumencie nie może być dwóch elementów z takim samym atrybutem id="...". Dlatego jeżeli chcemy wstawić drugie menu na tej samej stronie, trzeba dla niego użyć już zmodyfikowanego kodu, np.:
<ul id="tree1" class="tree"> ... </ul> <script> new Tree("tree1"); </script>
Aby menu prezentowało się w pełni funkcjonalnie, trzeba jeszcze przygotować trzy grafiki ikon i zapisać je w tym samym katalogu, co skrypt menu. Na przykład:
closed.gif - przedstawia ikonę zamkniętego folderu menu,
opened.gif - otwarty folder,
document.gif - dokument.
Jeżeli chcemy w jakiś sposób wyróżnić niektóre dokumenty na liście, można w tym celu przypisać im oddzielną ikonę, która wyraźnie odróżnia się od innych:
<ul id="tree0" class="tree"> <li><a>Folder 1</a> <ul> <li><a href="...">Dokument 1.1</a></li> <li><a href="...">Dokument 1.2</a></li> </ul> </li> <li><a href="...">Dokument 2</a></li> <li style="background-image: url(document1.gif)"><a href="...">Dokument 3</a></li> <li style="background-image: url(document2.gif)"><a href="...">Dokument 4</a></li> </ul>
Na koniec warto dodać, że opisywany tutaj skrypt posiada system automatycznego wykrywania aktualnie załadowanej strony. Dzięki niemu po wczytaniu dokumentu wybranego na liście, gałąź w drzewie menu w której znajduje się odpowiadający link jest na starcie rozwinięta, a sam link zostaje wyróżniony przy użyciu klasy CSS pod nazwą active, co natychmiast wskazuje użytkownikowi, na której podstronie teraz się znajduje, a także ułatwia nawigację.
Kolekcje dokumentów
Zwykle kiedy publikowany artykuł jest dość długi, dzieli się go na osobne części, dzięki czemu czytelnik nie będzie musiał wczytywać całego tekstu od razu. W takim przypadku w menu umieszcza się najczęściej link tylko do pierwszej części artykułu, a na końcu treści wstawia nawigację stronicującą. Niestety ponieważ adres URL każdej kolejnej części podzielonego artykułu jest różny, właściwa gałąź menu zostanie otwarta i link zaznaczony tylko na pierwszej ze stron. Jeżeli jednak poinformujemy skrypt, które adresy wchodzą w skład kolekcji powiązanych dokumentów, po wejściu na kolejne części artykułu, wszystko będzie działać zgodnie z oczekiwaniami. Aby to zrobić, należy nadać właściwemu odnośnikowi z menu atrybut rel="Collection(...)", w którym podaje się listę adresów URL wszystkich części podzielonego artykułu:
<ul id="tree0" class="tree"> <li><a href="czesc1.html" rel="Collection(czesc2.html,czesc3.html)">Artykuł stronicowany</a></li> <li><a href="...">Dokument 2</a></li> <li><a href="...">Dokument 3</a></li> </ul>
Zwracam uwagę, że w nawiasie nie może być żadnych spacji - nawet po znaku przecinka! Jeśli w nazwie plików z kolekcji znajduje się spacja, należy ją zastąpić przez: %20. Podobnie znaki przecinka, znajdujące się w nazwach plików, należy zastąpić przez %2C, nawias otwierający - %28, nawias zamykający - %29.
Wyrażenia regularne
Występują sytuacje, kiedy dokumentów w kolekcji może być bardzo dużo albo nie wiemy z góry ile, wszystkie jednak pasują do określonego wzorca - np.: /artykul/czesc1.html, /artykul/czesc2.html, /artykul/czesc3.html itd. W takim przypadku dogodniejsze może być dopasowanie adresu przy użyciu wyrażeń regularnych. Aby to zrobić, adres w kolekcji należy objąć nawiasami kwadratowymi, w których wpisuje się treść wyrażenia, np.: rel="Collection([/artykul/czesc\d+\.html])". Również w tym przypadku przypominam, że w nawiasie kolekcji nie może być żadnych spacji, dodatkowych przecinków ani nawiasów okrągłych!