Rozdział 12. Obiektowy model dokumentu

16 stycznia 2013
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

W rozdziale 11 używaliśmy obiektów JavaScript odnoszących się do elementów form i input dokumentu HTML. Obiekty te należą do struktury o nazwie DOM (ang. Document-Object Model — obiektowy model dokumentu). W modelu tym reprezentację ma każdy element znajdujący się w dokumencie. Można go tam znaleźć i coś z nim zrobić.

Dokumenty HTML mają strukturę hierarchiczną. Każdy element (lub znacznik) z wyjątkiem głównego elementu <html> znajduje się w innym elemencie, który jest jego rodzicem. Element mający rodzica również może zawierać elementy potomne. Można to sobie wyobrazić, jako drzewo rodzinne.

Drzewo dokumentu HTML

Obiektowy model dokumentu opiera się właśnie na takiej reprezentacji dokumentu. Zwróć uwagę, że przedstawione drzewo zawiera dwa rodzaje elementów: węzły reprezentowane przez niebieskie pola i fragmenty zwykłego tekstu. Wkrótce się dowiesz, że urywki tekstu zachowują się trochę inaczej niż inne elementy. Na przykład nie mogą mieć dzieci.

Otwórz plik example_alchemy.html zawierający dokument przedstawiony na rysunku i powiąż go z konsolą.

attach(window.open("/wp-content/ejs/example_alchemy.html"));

Dostęp do obiektu stanowiącego korzeń drzewa dokumentu, węzła html, można uzyskać poprzez własność documentElement obiektu document. Najczęściej jednak potrzebny jest dostęp do części body dokumentu dostępnej jako document.body.


Łącza między tymi węzłami są dostępne jako własności obiektów węzłów. Każdy obiekt DOM ma własność parentNode, która odnosi się do obiektu, w którym ten obiekt się znajduje (jeżeli w ogóle ma rodzica). Ci rodzice również mają łącza wskazujące na ich dzieci, ale ponieważ dzieci może być wiele, są one przechowywane w pseudotablicy o nazwie childNodes.

show(document.body);
show(document.body.parentNode);
show(document.body.childNodes.length);

Dla wygody dostępne są też łącza o nazwach firstChild i lastChild wskazujące pierwszy i ostatni element dziecko w węźle lub null jeśli element nie zawiera żadnego elementu.

show(document.documentElement.firstChild);
show(document.documentElement.lastChild);

Ostatnie dwie własności to nextSibling i previousSibling wskazujące węzły znajdujące się „obok” określonego węzła ― są to węzły mające tego samego rodzica i znajdujące się przed lub za określonym elementem. Jeśli nie ma takiego elementu, własności zawierają wartość null.

show(document.body.previousSibling);
show(document.body.nextSibling);

Aby dowiedzieć się, czy wybrany węzeł reprezentuje tylko tekst czy węzeł HTML, można sprawdzić jego własność nodeType. Wartość 1 oznacza zwykły węzeł, a 3 węzeł tekstowy. Istnieją też inne rodzaje obiektów mające własność nodeType, np. obiekt document, którego wartość to 9, ale najczęściej używa się jej do odróżniania węzłów tekstowych od innych.

function isTextNode(node) {
  return node.nodeType == 3;
}

show(isTextNode(document.body));
show(isTextNode(document.body.firstChild.firstChild));

Zwykłe węzły mają własność o nazwie nodeName określającą typ reprezentowanego przez nie elementu HTML. Natomiast węzły tekstowe mają własność nodeValue zawierającą ich treść.

show(document.body.firstChild.nodeName);
show(document.body.firstChild.firstChild.nodeValue);

Nazwy węzłów są zawsze pisane wielkimi literami i trzeba to brać pod uwagę w wyrażeniach porównawczych.

function isImage(node) {
  return !isTextNode(node) && node.nodeName == "IMG";
}

show(isImage(document.body.lastChild));

Ćwiczenie 12.1

Napisz funkcję o nazwie asHTML pobierającą węzeł DOM i zwracającą łańcuch reprezentujący tekst HTML tego węzła i jego dzieci. Możesz zignorować atrybuty, tzn. wystarczy wyświetlić węzły w formie <nazwawezla>. Możesz używać funkcji escapeHTML z rozdziału 10, aby odpowiednio dostosować treść węzłów tekstowych.

Podpowiedź: Rekurencja!

function asHTML(node) {
  if (isTextNode(node))
    return escapeHTML(node.nodeValue);
  else if (node.childNodes.length == 0)
    return "<" + node.nodeName + "/>";
  else
    return "<" + node.nodeName + ">" +
           map(asHTML, node.childNodes).join("") +
           "</" + node.nodeName + ">";
}

print(asHTML(document.body));

W istocie węzły mają coś podobnego do funkcji asHTML. Przy użyciu własności innerHTML można pobierać tekst HTML z wnętrza węzła, bez znaczników samego węzła. Dodatkowo niektóre przeglądarki udostępniają też własność outerHTML, która zawiera również sam węzeł.

print(document.body.innerHTML);

Niektóre z tych własności można też modyfikować. Zmiana własności innerHTML zwykłego węzła albo nodeValue węzła tekstowego spowoduje zmianę ich treści. Należy podkreślić, że w pierwszym przypadku podany łańcuch jest interpretowany jako HTML, podczas gdy w drugim — jako zwykły tekst.

document.body.firstChild.firstChild.nodeValue =
  "Rozdział 1: Głęboka prawda ukryta w butelce";

Albo…

document.body.firstChild.innerHTML =
  "Znasz już element blink? <blink>Cudowny!</blink>";

Do tej pory dostęp do węzłów uzyskiwaliśmy przemierzając szeregi własności firstChild i lastChild. Tak też można, ale wymaga to dużo pisania i łatwo spowodować błąd. Jeśli na początku dokumentu wprowadzimy nowy węzeł, to document.body.firstChild nie będzie już odwoływać się do elementu h1 i kod, w którym przyjęto takie założenie przestanie poprawnie działać. Co więcej, niektóre przeglądarki dodają węzły tekstowe dla spacji i znaków nowego wiersza znajdujących się między elementami, a inne tego nie robią. Przez to drzewo DOM w każdej przeglądarce może być trochę inne.

Alternatywnym rozwiązaniem jest przypisanie każdemu elementowi, do którego chce się uzyskać dostęp atrybutu id. Na przykładowej stronie obraz ma identyfikator picture, przy użyciu którego możemy znaleźć ten element.

var picture = document.getElementById("picture");
show(picture.src);
picture.src = "/wp-content/uploads/ostrich.png";

Wpisując nazwę getElementById nie wpisz przez pomyłkę na końcu wielkiej litery. Ponadto, jeśli musisz ją często wpisywać, grozi Ci zespół cieśni kanału nadgarstka. Ponieważ nazwa document.getElementById jest o wiele za długa, jak na bardzo często używaną operację, programiści JavaScript maksymalnie ją skrócili do postaci $. Jak wiadomo znak $ jest w języku JavaScript literą, a więc może być używany jako nazwa zmiennej.

function $(id) {
  return document.getElementById(id);
}
show($("picture"));

Węzły DOM mają też metodę getElementsByTagName (kolejna fajna, krótka nazwa), która pobiera nazwę elementu i zwraca wszystkie węzły tego typu, jakie znajdują się w węźle, na rzecz którego została wywołana.

show(document.body.getElementsByTagName("BLINK")[0]);

Kolejną czynnością, jaką można wykonywać na drzewie DOM jest tworzenie nowych węzłów. Dzięki temu można w dowolnym momencie dodawać elementy do dokumentu, co pozwala uzyskać różne ciekawe efekty. Niestety interfejs jest niesamowicie niezgrabny. Można go jednak trochę poprawić używając paru funkcji pomocniczych.

Obiekt document ma metody createElement i createTextNode. Pierwsza służy do tworzenia zwykłych węzłów, a druga zgodnie z nazwą do tworzenia węzłów tekstowych.

var secondHeader = document.createElement("H1");
var secondTitle = document.createTextNode("Rozdział 2: Poważna magia");

Następnie wstawimy tytuł do elementu h1 i dodamy element do dokumentu. Najprostszym sposobem na zrobienie tego jest użycie metody appendChild, którą można wywołać na każdym nietekstowym węźle.

secondHeader.appendChild(secondTitle);
document.body.appendChild(secondHeader);

Nowym węzłom często przypisuje się jakieś atrybuty. Na przykład element img (obraz) jest bezużyteczny, jeśli nie ma atrybutu src wskazującego grafikę, która ma zostać wyświetlona. Większość atrybutów można traktować jako własności węzłów DOM, ale istnieją też metody setAttribute i getAttribute, które umożliwiają dostęp do atrybutów w bardziej ogólny sposób:

var newImage = document.createElement("IMG");
newImage.setAttribute("src", "/wp-content/uploads/Hiva-Oa.png");
document.body.appendChild(newImage);
show(newImage.getAttribute("src"));

Jednak gdy trzeba utworzyć większą liczbę węzłów, wielokrotne wywoływanie metody document.createElement lub document.createTextNode, a następnie dodawanie atrybutów i węzłów potomnych po jednym na raz jest bardzo żmudne. Na szczęście napisanie funkcji, która wszystko za nas zrobi nie jest trudne. Zanim to zrobimy, musimy zająć się jeszcze jednym drobiazgiem ― metoda setAttribute poprawnie działa w większości przeglądarek internetowych, ale w Internet Explorerze może sprawiać problemy. Nazwy niektórych atrybutów HTML mają w języku JavaScript specjalne znaczenie, przez co odpowiadające im nazwy własności obiektów są nieco zmodyfikowane. Atrybut class ma nazwę className, forhtmlFor, a checkeddefaultChecked. W Internet Explorerze metody setAttribute i getAttribute również działają z tymi zmienionymi nazwami, zamiast używać oryginalnych nazw z HTML-a, co może być mylące. Ponadto w przeglądarce tej atrybutu style, który razem z atrybutem class zostanie opisany w dalszej części tego rozdziału, nie można ustawiać przy użyciu metody setAttribute.

Obejście tego może wyglądać tak:

function setNodeAttribute(node, attribute, value) {
  if (attribute == "class")
    node.className = value;
  else if (attribute == "checked")
    node.defaultChecked = value;
  else if (attribute == "for")
    node.htmlFor = value;
  else if (attribute == "style")
    node.style.cssText = value;
  else
    node.setAttribute(attribute, value);
}

W każdym przypadku, w którym Internet Explorer odbiega od reszty przeglądarek robimy coś, co działa we wszystkich przypadkach. Nie przejmuj się szczegółami — jest to brzydka sztuczka, której wolelibyśmy nie stosować, ale zmuszają nas do tego niepokorne przeglądarki. Teraz możemy napisać prostą funkcję do tworzenia elementów DOM.

function dom(name, attributes) {
  var node = document.createElement(name);
  if (attributes) {
    forEachIn(attributes, function(name, value) {

      setNodeAttribute(node, name, value);
    });
  }
  for (var i = 2; i < arguments.length; i++) {
    var child = arguments[i];
    if (typeof child == "string")
      child = document.createTextNode(child);
    node.appendChild(child);
  }
  return node;
}

var newParagraph = 
  dom("P", null, "Akapit zawierający ",
      dom("A", {href: "http://en.wikipedia.org/wiki/Alchemy"},
          "łącze"),
      " wewnątrz.");
document.body.appendChild(newParagraph);

Funkcja dom tworzy węzeł DOM. Jej pierwszy argument określa nazwę elementu reprezentowanego przez tworzony węzeł, a drugi argument jest obiektem zawierającym atrybuty tego węzła lub wartość null, jeśli nie ma atrybutów. Dalej może znajdować się dowolna liczba argumentów, które zostaną dodane do węzła jako dzieci. Jeśli wśród nich znajdą się łańcuchy, to zostaną najpierw umieszczone w węźle tekstowym.


Metoda appendChild nie jest jedynym sposobem na wstawianie węzłów do innych węzłów. Jeśli nowy węzeł nie może znajdować się na końcu swojego rodzica, można użyć metody insertBefore, aby dodać węzeł przed innym węzłem dzieckiem. Nowy węzeł podaje się jako pierwszy argument, a istniejący — jako drugi.

var link = newParagraph.childNodes[1];
newParagraph.insertBefore(dom("STRONG", null, "great "), link);

Jeśli wstawi się gdzieś węzeł mający już rodzica (parentNode), węzeł ten automatycznie zostanie usunięty z dotychczasowego miejsca ― żaden węzeł nie może występować w drzewie dokumentu więcej niż raz.

Gdy trzeba zastąpić węzeł innym, należy użyć metody replaceChild, która jako pierwszy argument przyjmuje nowy węzeł, a jako drugi — stary.

newParagraph.replaceChild(document.createTextNode("luźne "),
                          newParagraph.childNodes[1]);

W końcu za pomocą metody removeChild usuwa się węzły potomne. Zwróć uwagę, że wywołuje się ją na rodzicu węzła, który ma zostać usunięty i przekazuje się jej element potomny jako argument.

newParagraph.removeChild(newParagraph.childNodes[1]);

Ćwiczenie 12.2

Napisz wygodną funkcję removeElement usuwającą przekazany jej w argumencie węzeł DOM z węzła nadrzędnego.

function removeElement(node) {
  if (node.parentNode)
    node.parentNode.removeChild(node);
}

removeElement(newParagraph);

Podczas tworzenia nowych węzłów i przenoszenia istniejących należy pamiętać o następującej zasadzie: węzłów nie można wstawiać do dokumentu innego niż ten, w którym zostały utworzone. Oznacza to, że jeśli są dodatkowe ramki lub otwarte okna, nie można pobrać części dokumentu z jednego z tych obiektów i przenieść go do innego oraz węzły utworzone przy użyciu metod jednego obiektu document muszą pozostać w tym dokumencie. Niektóre przeglądarki, a konkretnie Firefox, nie przestrzegają tej zasady, przez co kod ją łamiący w Firefoksie zadziała.


Przykładem użycia funkcji dom w jakimś pożytecznym celu jest program pobierający obiekty JavaScript i tworzący z nich tabelę. W języku HTML tabele tworzy się przy użyciu elementów, których nazwy zaczynają się od litery „t”, np.:

<table>
  <tbody>
    <tr> <th>Drzewo </th> <th>Kwiaty</th> </tr>
    <tr> <td>Jabłoń</td> <td>Białe  </td> </tr>
    <tr> <td>Koral</td> <td>Czerwone    </td> </tr>
    <tr> <td>Sosna </td> <td>Brak   </td> </tr>
  </tbody>
</table>

Elementy tr reprezentują wiersze tabeli. Elementy th i td to komórki, przy czym td to zwykłe komórki, a th to komórki nagłówka, których zawartość lekko się wyróżnia. Element tbody (ang. table body — treść tabeli) nie jest wymagany w samym języku HTML, ale musi być użyty, gdy tabelę tworzy się z węzłów DOM, ponieważ Internet Explorer nie wyświetla tabel utworzonych bez tego elementu.


Ćwiczenie 12.3

Funkcja makeTable przyjmuje jako argumenty dwie tablice. Pierwsza zawiera obiekty JavaScript, które mają być wstawione do tabeli, a druga — łańcuchy określające nazwy kolumn tej tabeli i własności obiektów, które mają zostać pokazane w tych kolumnach. Na przykład poniższe wywołanie tworzy wcześniej pokazaną tabelę:

makeTable([{Drzewo: "Jabłoń", Kwiaty: "Białe"},
           {Drzewo: "Koral", Kwiaty: "Czerwone"},
           {Drzewo: "Sosna",  Kwiaty: "Brak"}],
          ["Drzewo", "Kwiaty"]);

Napisz tę funkcję.

function makeTable(data, columns) {
  var headRow = dom("TR");
  forEach(columns, function(name) {
    headRow.appendChild(dom("TH", null, name));
  });

  var body = dom("TBODY", null, headRow);
  forEach(data, function(object) {
    var row = dom("TR");
    forEach(columns, function(name) {
      row.appendChild(dom("TD", null, String(object[name])));
    });
    body.appendChild(row);
  });

  return dom("TABLE", null, body);
}

var table = makeTable(document.body.childNodes,
                      ["nodeType", "tagName"]);
document.body.appendChild(table);

Nie zapomnij przekonwertować wartości z obiektów na łańcuchy przed ich dodaniem do tabeli ― nasza funkcja dom rozpoznaje tylko łańcuchy i węzły DOM.


Z tematem języka HTML i obiektowego modelu dokumentu ściśle powiązane są kaskadowe arkusze stylów. Jest to obszerna dziedzina, której nie sposób wyczerpująco opisać w tej publikacji, ale dzięki znajomości arkuszy stylów w języku JavaScript można robić wiele ciekawych rzeczy. Dlatego poniżej znajduje się opis podstawowych zagadnień.

Kiedyś jedynym sposobem na zmienianie wyglądu elementów HTML było przypisywanie im dodatkowych atrybutów albo umieszczanie ich w innych elementach, np. center aby wyśrodkować treść albo font aby zmienić właściwości czcionki. Gdy chciały się, aby wszystkie akapity albo tabele w dokumencie wyglądały w określony sposób, każdemu elementowi tego typu trzeba było dodać kilka atrybutów. To powodowało, że dokumenty były pełne niepotrzebnych znaczników oraz strasznie trudno się je pisało i modyfikowało.

Oczywiście ludzie to pomysłowe stworzenia i szybko wymyślili rozwiązanie tego problemu. Arkusze stylów to narzędzie do pisania instrukcji w rodzaju „w tym dokumencie wszystkie akapity mają być drukowane czcionką Comic Sans i mieć różowy kolor, a wszystkie tabele mają mieć grube zielone obramowanie”. Instrukcje te wpisuje się w jednym miejscu na początku dokumentu lub w osobnym pliku i mają one zastosowanie do całego dokumentu. Poniżej znajduje się przykładowy arkusz stylów, który wypośrodkowuje treść nagłówków i nadaje im rozmiar 22 punktów oraz definiuje opisane wcześniej ustawienia czcionki i koloru pisma dla wszystkich akapitów należących do klasy brzydal.

<style type="text/css">
  h1 {
    font-size: 22pt;
    text-align: center;
  }

  p.ugly {
    font-family: Comic Sans MS;
    color: purple;
  }
</style>

Z arkuszami stylów związane jest pojęcie klas. Jeśli na stronie znajdują się różne rodzaje akapitów, np. brzydkie i ładne, to nie należy definiować jednego stylu dla wszystkich elementów p, tylko użyć klas, aby je rozróżnić. Powyższy arkusz stylów zostanie zastosowany tylko do takich akapitów:

<p class="brzydal">Lustereczko, lustereczko...</p>

To właśnie tych klas dotyczy własność className, o której krótko nadmieniłem przy opisie funkcji setNodeAttribute. Za pomocą atrybutu style można dodać arkusz stylów bezpośrednio do elementu. Na przykład poniższa instrukcja definiuje czteropikselowe jednolite obramowanie dla elementu obrazu.

setNodeAttribute($("picture"), "style",
                 "border-width: 4px; border-style: solid;");

Technologia kaskadowych arkuszy stylów jest o wiele bardziej skomplikowana. Niektóre style są dziedziczone przez węzły potomne po rodzicach i reagują ze sobą nawzajem na wiele różnych sposobów, ale z punktu widzenia programisty posługującego się drzewem DOM, najważniejsze jest, aby wiedzieć, że każdy węzeł DOM ma własność style, za pomocą której można manipulować jego stylem oraz że jest kilka rodzajów stylów, przy użyciu których można zmusić węzły do robienia niezwykłych rzeczy.

Własność style odnosi się do obiektu zawierającego własności dla wszystkich elementów tego stylu. Możemy np. ustawić zielony kolor obramowania obrazu.

$("picture").style.borderColor = "green";
show($("picture").style.borderColor);

Zwróć uwagę, że w arkuszach stylów słowa są oddzielane łącznikiem, np. border-color, natomiast w JavaScripcie każde nowe słowo od drugiego rozpoczyna się wielką literą, np. borderColor.

Bardzo praktycznym stylem jest display: none. Za jego pomocą można czasowo ukryć wybrany węzeł: gdy własność style.display jest ustawiona na none, element nie jest wyświetlany w przeglądarce, mimo że istnieje. Później display można ustawić na pusty łańcuch, aby spowodować pojawienie się elementu.

$("picture").style.display = "none";

A teraz odzyskujemy nasz obrazek:

$("picture").style.display = "";

Kolejnym rodzajem stylów, które można wykorzystać na wiele ciekawych sposobów są style pozycjonujące. W prostym dokumencie HTML wszystkie elementy są rozmieszczane na ekranie przez przeglądarkę internetową ― każdy element jest ustawiany obok lub pod elementem znajdującym się przed nim w kodzie źródłowym i węzły z zasady nie nakładają się nawzajem.

Jeśli jednak węzłowi ustawi się styl position na absolute, to zostaje on wyjęty z tzw. układu normalnego (ang. normal flow). Nie zajmuje więcej miejsca w dokumencie, tylko jakby pływa nad nim. Jego położenie można ustawiać za pomocą własności left i top. Można to wykorzystać na wiele sposobów, od zmuszenia węzła do podążania za kursorem po tworzenie okien zasłaniających resztę strony.

$("picture").style.position = "absolute";
var angle = 0;
var spin = setInterval(function() {
  angle += 0.1;
  $("picture").style.left = (100 + 100 * Math.cos(angle)) + "px";
  $("picture").style.top = (100 + 100 * Math.sin(angle)) + "px";
}, 100);

Jeśli nie znasz się na trygonometrii, to musisz mi uwierzyć na słowo że kosinusa i sinusa używa się do obliczania współrzędnych punktów na obwodzie okręgu. Dziesięć razy na sekundę zmienia się kąt położenia obrazu i obliczane są nowe współrzędne. Częstym błędem popełnianym przy takiej pracy jest zapominanie o dodaniu jednostki px do wartości. W większości przypadków brak jednostki oznacza, że styl nie zadziała, a więc trzeba dodać px (piksele), % (procenty), em (1em oznacza szerokość litery M) lub pt (punkty).

(pozwólmy obrazkowi spocząć…)

clearInterval(spin);

Miejsce uważane ze punkt 0,0 do określania pozycji zależy od tego, gdzie w dokumencie znajduje się węzeł. Jeśli węzeł znajduje się w innym węźle mającym własność position: absolute lub position: relative, to punktem zerowym jest lewy górny róg tego węzła. W pozostałych przypadkach jest nim lewy górny róg dokumentu.


Jeśli przestudiowałeś wszystkie przykłady przedstawione w tym rozdziale i może sam też coś zrobiłeś, to dokument, w którym pracujesz został mocno sponiewierany. Może prawię morały, ale muszę Ci powiedzieć, że nie powinieneś tego robić z prawdziwymi stronami. Czasami może Cię kusić, żeby zastosować jakieś ruchome błyskotki. Ale oprzyj się tej pokusie albo Twoje strony staną się nieczytelne albo nawet będą powodować zawieszanie się przeglądarek.

Autor: Marijn Haverbeke

Źródło: http://eloquentjavascript.net/1st_edition/chapter12.html

Tłumaczenie: Łukasz Piwko

Treść tej strony dostępna jest na zasadach licencji CC BY 3.0

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *