Strona początkowa

Spacer po hierarchii

Wiemy już, że elementy na stronie tworzą hierarchiczne drzewo. Aby operować na takich obiektach, musimy dobrze opanować sztukę "spacerowania" po nich ^^.

Każdy element na stronie tworzy tak zwany zwaną node czyli pojedynczy korzeń drzewa. Takimi nodami są nie tylko Tagi, ale także tekst w nich zawarty, ich atrybuty itp.

Przykładowo w kodzie:

	<p> Jakiś tekst </p>
	

nodem jest zarówno akapit jak i nodem jest tekst, który się w nim znajduje.

Niektóre typy nodów mogą zawierać w sobie nody - dzieci (child), a niektóre dzieci mieć nie mogą (na przykład takim nodem będzie znak łamania linii <br /> lub <input />) ;). Jeżeli dany element posiada atrybuty (np src, alt), to są one jego nodami.

Pobieranie nodów

Aby pobrać noda, możemy skorzystać z dwóch podstawowych metod oferowanych przez JS - getElementById i getElementsByTagName. Pierwsza z nich zwraca odwołanie do elementu, który posiada dany ID, druga zaś pobiera nam wszystkie elementy danego typu w postaci tablicy.

Zastosowanie tych metod jest bardzo proste:

	<input type="button" id="guzik" value="ok" />

	<script type="text/javascript">
		var g = document.getElementById('guzik');
		alert(g.value); //korzystamy z okienka dialogowego by wypisać value pobranego guzika
	</script>
	

Zauważyłeś, że pobrany element podstawiam po zmienną?
Gdy pobieram element z dokumentu, zmuszam przeglądarkę do przejścia po wszystkich elementach strony i wyszukania szukanego elementu.
Dzięki temu, że pobrany element podstawiam pod zmienną, przy kolejnej operacji (w naszym przykładzie odczycie wartości value) znacząco zyskuję na szybkości, gdyż przeglądarka nie musi kolejny raz szukać.

W przypadku powyższego skryptu różnicy nie zauważymy. Ale w przypadku większych skryptów różnica może już być znacząca.

Kolejna omawiana metoda to getElementsByTagName:

	<table id="tabelka">
		<tr><td>1</td>2<td>3</td></tr>
		<tr><td>4</td>5<td>6</td></tr>
	</table>

	<script type="text/javascript">
		var tab = document.getElementById('tabelka');
		var td = tab.getElementsByTagName('td'); //pobieramy wszystkie td z tabeli
		for (x=0; x<td.length; x++) { //pętla po wszystkich td 
			td[x] = ... //tutaj czarujemy z każdym td
		}
	</script>
	

Do pobrania elementu o danej klasie css, skorzystamy z metody getElementsByTagName. Wystarczy pobrać elementy, po czym wykonać po nich pętlę, zwracając te z wymaganą klasą:

	function getElementsByClass(element, tagName, className) {
		var pasujace = [];
		var dzieci = element.getElementsByTagName(tagName);
		for (var i = 0; max = dzieci.length; i < max; i++) {
			if ((new RegExp(className)).test(dzieci[i].className)) {
				pasujace.push(dzieci[i]);
			}
		}
		return pasujace;
	}
	

Czemu podczas definiowania pętli for podstawiliśmy długość kolekcji pod dodatkową zmienną max? Dzięki temu nasz skrypt w większości przeglądarek wykona się o wiele lepiej (w starszych IE nawet 170x szybciej!).
W praktyce nie jest to zbyt często stosowana metoda - co nie znaczy, że nie warto jej znać :)

	var body = document.getElementsByTagName('body')[0];
	var superTabelki = getElementsByClass(body, 'table', 'super'); //pobieramy tabele z body, które mają klasę "super"
	var czerwone =  getElementsByClass(body, '*', 'czerwone'); //pobieramy wszystkie tagi z body, które mają klasę "czerwone"
	

W nowoczesnych przegladarkach istnieją też o wiele bardziej pomocne metody, które pobierają elementy z danymi ID lub klasami CSS - zupełnie jak w jQuery.

Pierwsza z omawianych metod to querySelector(), która zwraca pierwszy element z pasujących. Jako jej argumenty podajemy wyrażenie CSS, które określa szukane elementy:

 
		//pobieramy pierwszy element na stronie z klasą .wazne
		var element = document.querySelector('.wazne, .inne');
	

Druga metoda to querySelectorAll(), która ma bardzo podobne działanie, z tą różnicą, że pobiera wszystkie pasujące elementy:

		//pobieramy wszystkie elementy, które mają klasy .wazne, .inne oraz element z id=main
		var elementy = document.querySelectorAll('.wazne, .inne, #main');
		if(elementy) {
			var element = elementy[0];
			
			for (i=0; max = elementy.length; i<elementy.length; i++) {
				console.log(elementy[i]);
			}
		}
	

Wszyscy ci, którzy przyzwyczaili się do używania jQuery, mogą pokusić się do napisania małego skryptu, który ułatwi im pracę z powyższymi metodami:

	function $(selector, el) {
		if (!el) {el = document;}
		return el.querySelectorAll(selector);
	}
	
	alert( $('.z_kolorami')[0].id );
	

Dodatkowe informacje na ten temat znajdziecie tutaj

Relacje między nodami

Rozpiszmy przykładowy akapit, który posiada w sobie pogrubiony tekst:

To jest bardzo ważny tekst

	<p id="paragraf">To jest <strong>bardzo</strong> ważny tekst</p>
	

Nasz paragraf możemy w łatwy sposób rozpisać za pomocą wykresu:

rozpisany akapit

Znając hierarchiczne położenie obiektów, możemy w łatwy sposób się po nich przemieszczać:

	var p = document.getElementById('paragraf');
	p.parentNode //wskazuje na rodzica czyli na body - jeżeli dany paragraf jest w body
	p.childNodes[1] //wskazuje na drugie dziecko noda czyli <strong>

	//jeżeli nasz akapit będzie pierwszy na stronie to...
	
	document.firstChild.childNodes[1].firstChild.nodeValue //pobranie tekstu To jest
	document.firstChild.childNodes[1].parentNode.childNodes[1].firstChild.nodeValue //To samo co wyżej - czemu?
	

Każdy nod posiada pewne właściwości i swój typ. I tak np. właściwość nodeType dla tekstu ma wartość 3, a dla przycisku na stronie ma wartość 1. typy nodów i ich właściwości są tylko do odczytu.

Istnieje kilka właściwości przypisanych do każdego node:
WłaściwośćOpis
nodeNamenazwa node
nodeValuewartosć node (tylko dla nodów tekstowych)
nodeTypetyp node - zobacz niżej
parentNode rodzic (parent), jeżeli istnieje
childNodes lista (array) dzieci danego obiektu (child nodes)
firstChild pierwsze dziecko (first child)
lastChild ostatnie dziecko (last child)
previousSibling zwraca poprzedni nod na tym samym poziomie
nextSibling zwraca następny nod na tym samym poziomie
attributes lista atrybutów elementu
ownerDocument dokument zawierający ten element
Typy nodów:
NumberOpis
1element HTML
2atrybut elementu
3tekst
8zawartość HTML
9kodument
10dokument definicji

Zastosowanie w praktyce

Poniższy przykład pokazuje zastosowanie powyższych informacji w praktyce:

	<table id="tabelka" class="tab">
		<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td></tr>
		<tr><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td><td>12</td></tr>
		<tr><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td></tr>
		<tr><td>19</td><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td></tr>
	</table>
	
	function podswietl_wiersze(id) {
		var tab = document.getElementById(id);
		var td = tab.getElementsByTagName('td');
		for (x=0; x<td.length; x++) {
			td[x].onmouseover = function() {
				this.parentNode.className = 'podswietlone'; //ustawiamy klase hover dla TR
			}
			td[x].onmouseout = function() {
				this.parentNode.className = '';
			}
		}
	}

	podswietl_wiersze('tabelka')
	
123456
789101112
131415161718
192021222324

Atrybuty jako nody

Jak powiedziałem na początku, atrybuty każdego elementu też są jego nodami. Tak więc dla każdego atrybutu możemy korzystać z właściwości nodów:

	for( var x = 0; x < p.attributes.length; x++ ) {
		if( p.attributes[x].nodeName.toLowerCase() == 'title' ) {
			alert( 'Wartość atrybutu "title" wynosi: ' + p.attributes[x].nodeValue );
		}
	}
	

Aby pobrać atrybut elementu możemy skorzystać z metody getAttribute(atrybut). Atrybut może istnieć i być pusty, jednak nie przeszkadza to by sprawdzić jego istnienie:

	alert( 'Wartość atrybutu "title" wynosi: ' + p.getAttribute('title') );

	//sprawdzamy czy atrybut istnieje
	if (!p.getAttribute('title')) {
		...atrybut nie istnieje
	}
	

Aby ustawić wartość atrybutu możemy skorzystać z setAttribute(atrybut). Nazwy atrybutów tworzymy tak samo jak w przypadku nazw css dla javascript, tak więc aby zmienić atrybut bgcolor, musimy go podać jako bgColor.

	td.setAttribute('bgColor','red');
	

Do usuwania atrybutu służy metoda removeAttribute(atrybut).

	td.removeAttribute('bgColor');
	td.className = 'czerwona';
	

W IE7 (i kilku innych przeglądarkach) za pomocą metody setAttribute() nie możemy ustawiać atrybutów style, class oraz zdarzeń inilne. W IE8 zostało to poprawione, jednak zdarzeń wciąż nie możemy ustawiać (co w zasadzie problemem nie jest, gdyż i tak tego typu deklaracji super bohaterowie nie lubią).

Nody tekstowe

Spróbujmy teraz odczytać wartość noda tekstowego:

	alert( 'Wartość tekstu: ' + p[3].firstChild.nodeValue );
	

W powyższym kodzie odczytaliśmy wartość tekstu w 4 akapicie. Zwróć uwagę, że czytaliśmy wartość pierwszego noda dla tego akapitu. Tekst sam w sobie jest nodem.

Teoretycznie pobrany tekst powinien być zwrócony w całości, niezależnie od jego długości. Teoria jednak nie zawsze idzie w parze z praktyką. W Operze 7-9, wielkość pobranego tekstu wynosi 32kb, w starszych przeglądarkach z serii Mozilla wynosi tylko 4kb. Jeżeli wielkość tekstu przekroczy te magiczne wartości, zostanie on podzielony na części - nody tekstowe. Dlatego czasami przy pobieraniu długich tekstów, musimy mieć na uwadze, że będziemy musieli zrobić pętlę po nodach.

	var p = document.getElementsByTagName('p')[0]; //akapit z tekstem Pana Tadeusza
	var tekst = '';
	if (p.childNodes && p.childNodes.length>1) {
		for (i=0; i<p.childNodes.length; i++) {
			var tekst += p.childNodes[i].nodeValue; 
		}
	}
	

W nowszych przeglądarkach na szczęście ten problem nie istnieje.

Problemy z białymi znakami

Spacerując po dworzu czasami możesz trafić na pewne problemy. A to się człowiek przewróci, a to dostanie niechcący w "twarz". W JS niestety też się takie rzeczy zdarzają.

Rozpatrzmy pewien kod:

	<p>  <span>Ala ma kota, a kot ma Alę.</span> <strong>Ala go kocha</strong> <span>a kot ją wcale...</span>  </p>
	

Jak pobrać powyższe znaczniki? Bardzo prosto:

	var p = document.getElementsByTagName('p')[0]; //p - nasz akapit
	p.firstChild //wskazuje na span
	p.childNodes[1] //wskazuje na strong
	p.childNodes[2] //wskazuje na drugi span
	

Okazuje się jednak, że powyższy kod zadziała tylko w przeglądarce IE. Dzieje się tak dlatego, ponieważ między kolejnymi znacznikami występują białe znaki (tabulacja, spacje, czy łamanie linii), które przecież też są rodzajem tekstu, a więc i pełnoprawnymi nodami. Niestety IE spacerując po drzewie dokumentu pomija nody zawierające tylko białe znaki, co owocuje kolejną niekompatybilnością z inymi przeglądarkami.

"No to jesteśmy załatwieni".
Nie do końca. Wyjść jest kilka. Przede wszystkim zawsze możemy skorzystać z metod getElementById lub getElementsByTagName - to pewniaki. Drugim wyjściem jest usunąć z drzewa dokumentu wszystkie nody z białymi znakami:

	function usunBialeZnaki(element) {
		for (x=0; x<element.childNodes; x++) {
			if (element[x].nodeType==1 && element[x].test(/^\s\n\r/)) {
				element[x].parentNode.removeChild(element[x])
			} else if (element[x].nodeType==3 && element[x].chasChildNodes()) {
				usunBialeZnaki(element[x])
			}
		}
	}

	usunBialeZnaki(document);
	

Więcej na ten temat możesz dowiedzieć się tutaj: http://developer.mozilla.org/en/docs/Whitespace_in_the_DOM.