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!