Глава 13
Document Object Model

Когато отворите уеб страница в браузъра си, той извлича текста на HTML страницата и го прави по избор, който много прилича на начина, по който нашия анализатор от Chapter 11 анализира програми. Браузърът изгражда модел на структурата на документа и след това използва този модел за да направи страницата на екрана.

Този начин на представяне на документа е една от играчките, които програмите на JavaScript имат на разположение в пясъчника му. Може да четете от модела и да го променяте. Той действа, като една жива структура от данни : когато е модифициран, на страницата на екрана се актуализира, за да отрази промените.

Структура на документа

Можете да си представите един HTML документ, като вложен набор от кутии. Тагове като, <body> и </body> прилагат други тагове, които от своя страна съдържат други тагове или текст. Ето, например документа от предишната глава:

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>

Тази страница има следната структура:

HTML document as nested boxes

Структурата на данните, които браузъра използва за представяне на документа. следват тази форма. За всяка кутия там има един обект, с който можете да взаимодействате, за да научите неща, като това, което таговете на HTML представят и какви кутии и текст съдържа. Това представяне се нарича Document Object Model или DOM за по-кратко.

Глобалната променлива document ни дава достъп до тези обекти. Нейното свойство documentElement се отнася до обекта представен с <html> тага. Той също така предоставя свойствата head и body, които държат обектите за тези елементи.

Trees

Спомнете си синтактичните дървета от Глава 11 за момент. Техните структури са поразително сходни със структурата на документ за браузъра. Всяко разклонение може да се отнася към други разклонения - children деца, които от своя страна могат да имат свои деца. Тази форма е типична за вложени структури, където елементите могат да съдържат под-елементи подобни на тях

Ние наричаме структурата от данни - tree (дърво), когато има разклонена структура, но все още няма цикли (разклонение не може да се съдържа директно или косвено) но има един добре дефиниран “корен”. В случай на DOM, document.documentElement служи, като корен.

Дърветата навлизат в много компютърни науки. В допълнение към представляващи рекурсивни структури, като HTML документи или програми, те често се използват за поддържане на сортираните набори от данни, защото елементите обикновено могат да бъдат намерени или вмъкнати по-ефективно в сортирано дърво, отколкото в сортиран плосък масив.

Типично дърво има различни видове разклонения. Дървото на синтаксиса на езика Egg имаше променливи, стойности и прилагне на разклонения. Приложните разклонения винаги имат деца, докато стойности и променливи са, като листа или разклонения без деца.

Същото важи и за DOM. Разклоненията са редовни elements (елементи), които представляват HTML тагове, които определят структурата на документа. Те могат да имат деца разклонения. Пример за такова разклонение е document.body. Някои от тези деца могат да бъдат листа разклонения, като части от текст или коментари (коментарите в HTML се пишат между <!-- и -->).

Всеки DOM обект разклонение има nodeType свойство, което съдържа цифров код, който идентифицира типа на разклонението. Нормалните елементи имат стойност (1), което също така е дефинирано, като постоянна константа на document.ELEMENT_NODE. Текстовите разклонения, които представляват част от текст в документа имат стойност (3) (document.TEXT_NODE). Коментарите имат стойност (8) (document.COMMENT_NODE).

Това е още един начин да се визуализира дървото на нашия документ:

HTML document as a tree

Листата са текстови разклонения, стрелките показват връзката между родител - дете и отношенията между разклоненията.

Стандартът

Използвайки тайни числови кодове за представяне на видовете разклонения не е много в стила на JavaScript. По-късно в тази глава ще разгледаме други части от интерфейса на DOM, които също се чувстват тромави и чужди. Причината за това е, че DOM не е проектиран само за JavaScript. Вместо това той се опитва да определи неутрален интерфейс, който може да се използва с други системи, не само с HTML но и с XML, който има общ формат със синтаксиса на HTML.

Това е жалко. Стандартите често са полезни. Но в този случай предимството не е толкова убедително. Както един интерфейс, който правилно е интегриран с езика, който използвате, ще ви спести повече време, отколкото познатия интерфейс на различни езици.

Като пример за такава слаба интеграция, помислете за свойството childNodes, което елементите на разклоненията в DOM имат. Това свойство притежава масиво-подобен обект с length свойство и свойства белязани с номера за достъп до childs разклоненията. Но това е инстанция на типа NodeList, а не на реален масив, така че не разполага с методи, като slice и forEach.

После идват въпросите за лош дизайн. Например, няма начин за създаване на ново разклонение и веднага да се добавят деца или атрибути към него. Вместо това трябва първо да го създадете и след това да добавите децата едно по едно и най-накрая атрибутите един по един, като използвате страничните ефекти. Код, който взаимодейства силно с DOM има тенденцията да е дълъг, повтарящ се и грозен.

Но тези недостатъци не са фатални. Тъй като, JavaScript ни позволява да създаваме собствени абстракции, с които лесно да напишем няколко помощни функции, които да ни позволят да изразим извършваните операции по по-ясен и по-кратък начин. В действителност много библиотеки, предназначени за програмиране на браузъри идват с такива инструменти.

Предвижване през дървото

DOM разклоненията съдържат доста връзки към други близки разклонения. Следната диаграма илюстрира това:

Links between DOM nodes

Въпреки, че диаграмата показва само една връзка от всички типове, всяко разклонение има parentNode свойство, което сочи към съдържащото го разклонение. По същия начин всеки елемент на разклонение (тип разклонение 1) има childNodes свойство, което сочи към масиво-подобен обект, който държи неговите деца.

На теория можете да се преместите навсякъде в дървото само с помощта на връзките между деца и родители. Но JavaScript ви дава също достъп до редица допълнителни удобни връзки. Свойствата firstChild и lastChild са връзки към първия и последния елемент в разклонението и имат стойност null за разклонения без деца. По същия начин previousSibling и nextSibling са връзки към съседни разклонения, които имат връзка с общ родител и се появяват непосредствено преди и след разклонението. За първото дете previousSibling ще бъде нула и за последното дете, nextSibling също ще бъде нула.

Когато става въпрос за вложени структури от данни, като тези, рекурсивните функции често са полезни. Следната рекурсивна функция сканира документ за текстови разклонения, съдържащи се в даден string връща true, когато е установила едно.

function talksAbout(node, string) {
  if (node.nodeType == document.ELEMENT_NODE) {
    for (var i = 0; i < node.childNodes.length; i++) {
      if (talksAbout(node.childNodes[i], string))
        return true;
    }
    return false;
  } else if (node.nodeType == document.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

Свойството nodeValue на текстово разклонение се отнася до текстовия string, който представлява.

Намиране на елементи

Навигирането с тези връзки между родители, деца, братя и сестри често е полезно, както предходната функция, която минава през целия документ. Но ако искаме да намерим конкретно разклонение в документа, чрез стартиране на document.body и сляпото следване на твърдия кодов път на връзките е лоша идея. По този начин, втвърдяваме предположенията в нашата програма относно прецизната структура на документа- структура която можем да искаме да променим по-късно. Друг усложняващ фактор е, че текстови разклонения се създават дори за празното пространство между разклоненията. В примерния документ, body таговете не разполагат само с три деца, едно (<h1> и две <p>), а всъщност със седем: тези три плюс пространствата преди, след и между тях.

Така че, ако искаме да получим href атрибута на линка в този документ, ние не искаме да кажем нещо като: "вземи второто дете на шестото дете на body в документа". По-лесно би било, ако можехме да се кажем “вземи първата връзка”. И можем:

var link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

Всички елементи на разклонения имат метода getElementsByTagName, който събира всички елементи с името на даден таг, които са деца (преки или косвени) на дадено разклонение и ги връща, като масиво-подобен обект.

За да намерите едно конкретно разклонение може да му дадете id атрибут и да използвате document.getElementById.

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>

<script>
  var ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

Третият подобен метод е getElementsByClassName, който подобно на getElementsByTagName, претърсва съдържанието на разклоненията и извлича всички елементи, които имат дадения string в техиня class атрибут.

Промяна на документа

Почти всичко в структурата на данните в DOM може да се променя. Разклоненията на елементите имат редица методи, които могат да бъдат използвани за промяна на тяхното съдържание. Метода removeChild премахва разклонението на дадено дете от документа. За добавяне на дете можем да използваме appendChild, който вмъква разклонение в края на списъка от деца или insertBefore, което вмъква разклонение, като първи аргумент преди разклонението дадено, като втори аргумент.

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  var paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

Разклонение може да съществува в документа само на едно място. По този начин, вмъкване на точка “Three” пред параграф “One” първо ще го премахне от края на документа и след това ще го постави отпред, което води до “Three/One/Two”. Всички операции, които вмъкват разклонение някъде, като страничен ефект причиняват отстраняване от сегашната позиция (ако има такава).

Метода replaceChild се използва за замяна на дадено дете разклонение с друго. Той приема, като аргументи две разклонения: новото разклонение и това, което трябва да бъде заменено. Замененото разклонение трябва да бъде дете на елемента върху, който е извикан метода. Имайте в пред вид, че и двата метода replaceChild и insertBefore очакват новото разклонение, като първи аргумент.

Създаване на разклонения

В следващия пример, ние искаме да напишем скрипт, който замества всички изображения (<img> тагове) в документа с текста съдържан в техните атрибути, в който се посочва алтернативно текстово представяне на изображението.

Това включва не само премахване на изображенията, но и добавяне на ново текстово разклонение, което да ги замени. За тази цел използваме метода document.createTextNode.

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>

<p><button onclick="replaceImages()">Replace</button></p>

<script>
  function replaceImages() {
    var images = document.body.getElementsByTagName("img");
    for (var i = images.length - 1; i >= 0; i--) {
      var image = images[i];
      if (image.alt) {
        var text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

При даден string, createTextNode ни дава разклонение от тип (3) на DOM (текстово разклонение), което можем да вмъкнем в документа за да се покаже на екрана.

Цикълът, който преминава над изображенията започва от края на списъка с разклонения. Това е необходимо, защото списъкът на разклоненията, върнат с метод подобен на getElementsByTagName (или свойство, като childNodes) е жив. Тоест, той се актуализира, когато се промени документа. Ако започнем от предната част с отстраняване на първото изображение, списъка ще изгуби своя първи елемент, така че при втората итерация на цикъла i ще бъде равна на 1 и ще спре, защото дължината на колекцията сега също ще е 1.

Ако искате солидна колекция от разклонения, за разлика от жива, може да превърнете колекция в истински масив, като извикате метода slice за масиви върху него.

var arrayish = {0: "one", 1: "two", length: 2};
var real = Array.prototype.slice.call(arrayish, 0);
real.forEach(function(elt) { console.log(elt); });
// → one
//   two

За да създадете редовни елементи - разклонения тип (1) можете да използвате метода document.createElement. Този метод взема името на тага и връща ново празно разклонение на дадения тип.

Следващия пример дефинира удобен elt, който създава елемент -разклонение и третира останалата част от аргументите си, като деца на това разклонение. Тази функция се използва за лесно добавяне на атрибут към цитат.

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>

<script>
  function elt(type) {
    var node = document.createElement(type);
    for (var i = 1; i < arguments.length; i++) {
      var child = arguments[i];
      if (typeof child == "string")
        child = document.createTextNode(child);
      node.appendChild(child);
    }
    return node;
  }

  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second editon of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

Атрибути

Някои атрибути на елементи, като например href за връзка, могат да бъдат достъпни чрез свойство със същото име на елемента в DOM обекта. Това е случай на ограничен набор от използвани стандартни атрибути.

Но HTML ви позволява да зададете всеки атрибут, който искате на разклонения. Това може да бъде полезно, тъй като ви позволява да съхранявате допълнителна информация в документа. Ако направите свои собствени имена на атрибути, обаче, такива атрибути няма да бъдат налични, като свойство на разклонението на елемента. Вместо това ще трябва да използвате методите getAttribute и setAttribute за работа с тях.

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
  var paras = document.body.getElementsByTagName("p");
  Array.prototype.forEach.call(paras, function(para) {
    if (para.getAttribute("data-classified") == "secret")
      para.parentNode.removeChild(para);
  });
</script>

Препоръчвам ви да пишете с префикс data- имената на такива измислени атрибути, за да гарантирате, че те няма да са в противоречие с всякави други атрибути.

Като прост пример, ще напишем “синтаксис за маркиране”, който търси <pre> тагове (предварително форматирани използвани за код и подобно на прав текст) с data-language атрибут, като грубо се опитаме да подчертаем ключовите думи в този език.

function highlightCode(node, keywords) {
  var text = node.textContent;
  node.textContent = ""; // Clear the node

  var match, pos = 0;
  while (match = keywords.exec(text)) {
    var before = text.slice(pos, match.index);
    node.appendChild(document.createTextNode(before));
    var strong = document.createElement("strong");
    strong.appendChild(document.createTextNode(match[0]));
    node.appendChild(strong);
    pos = keywords.lastIndex;
  }
  var after = text.slice(pos);
  node.appendChild(document.createTextNode(after));
}

Функцията highlightCode взема разклонение <pre> и регулярен израз (с включена глобална опция), който съвпада с ключовите думи на езика за програмиране, които елемента съдържа.

Свойството textContent се използва за да получим целия текст в разклонението и след това го настройваме към празен string, което има ефект на изпразване на разклонението. Цикъла минава над всички съответствия на израза с ключовата дума, добавя текст между тях, като редовно текстово разклонение, както и съвпадащия текст (ключовата дума), като текстови разклонения увити в <strong> (bold) елементи.

Ние можем автоматично да маркираме всички програми на страницата с цикъл върху всички <pre> елементи, които имат data-language атрибут и извикаме highlightCode върху всеки един с правилния израз за езика.

var languages = {
  javascript: /\b(function|return|var)\b/g /* … etc */
};

function highlightAllCode() {
  var pres = document.body.getElementsByTagName("pre");
  for (var i = 0; i < pres.length; i++) {
    var pre = pres[i];
    var lang = pre.getAttribute("data-language");
    if (languages.hasOwnProperty(lang))
      highlightCode(pre, languages[lang]);
  }
}

Ето един пример:

<p>Here it is, the identity function:</p>
<pre data-language="javascript">
function id(x) { return x; }
</pre>

<script>highlightAllCode();</script>

Има един често използван атрибут class, който е запазена дума в езика JavaScript. По исторически причини - стари приложения на JavaScript не могат да се справят с имената на свойствата, които съвпадат с ключови или запазени думи - това свойството, което се използва за достъп до този атрибут се нарича className. Можете също да получите достъп до него под реалното му име class с помощта на методите getAttribute и setAttribute.

Разположение на нещата

Може би сте забелязали, че различните видове елементи са представени по различен начин. Някои от тях, като параграфи (<p>) или заглавия (<h1>) заемат цялата ширина на документа и са поставени на отделни редове. Те се наричат block (блокови) елементи. Други елементи, като линкове (<a>) или <strong> използвани в предишния пример, са поставени на същия ред със техния обграден текст. Такива елементи се наричат inline елементи.

За всеки даден документ, браузъра е в състояние да изчисли разположението му, като дава на всеки елемент размер и позиция на базата на неговия вид и съдържание. Този план се използва за действителното изготвяне на документа.

Размерът и позицията на даден елемент може да бъде достъпен с JavaScript. Свойствата offsetWidth и offsetHeight дават пространството, което един елемент заема в pixels (пиксели). Пикселът е основна единица за измерване в браузъра и отговаря на една малка точка на вашия екран. По същия начин clientWidth и clientHeight дават размера на пространството вътре в елемента, като игнорира дебелината на границите.

<p style="border: 3px solid red">
  I'm boxed in
</p>

<script>
  var para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  console.log("offsetHeight:", para.offsetHeight);
</script>

Най-ефективния начин за намиране на най-точната позиция на елемент върху екрана е с метода getBoundingClientRect. Той връща обект с top,bottom, left и right свойства, като посочва в пиксели позициите на страните на елемента спрямо горния ляв ъгъл на екрана. Ако го искате по отношение на целия документ, трябва да добавите текущата скрол позиция, намерена с глобалните pageXOffset и pageYOffset променливи.

Разположението в документ може да бъде доста работа. В интерес на скоростта, браузърът не опреснява веднага промените в документа, когато той се променя, а по-скоро изчаква толкова дълго, колкото може. Когато програма на JavaScript, която променя документа завърши работа, браузърът ще трябва да изчисли новото разположение, за да покаже променения документ на екрана. Когато дадена програма пита за позицията или размера на нещо, четейки качествата, които offsetHeight или getBoundingClientRect предоставят, тя също се нуждае от изчисляване на разположението.

Програма, която многократно редува четене на DOM информация за разположение и променя DOM по това разположение ще работи наистина бавно. Следният код показва пример за това. Той съдържа две различни програми, които изграждат една линия с X характера, широка 2000 пиксела и измерва времето на добавянето на всеки един характер.

<p><span id="one"></span></p>
<p><span id="two"></span></p>

<script>
  function time(name, action) {
    var start = Date.now(); // Current time in milliseconds
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }

  time("naive", function() {
    var target = document.getElementById("one");
    while (target.offsetWidth < 2000)
      target.appendChild(document.createTextNode("X"));
  });
  // → naive took 32 ms

  time("clever", function() {
    var target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    var total = Math.ceil(2000 / (target.offsetWidth / 5));
    for (var i = 5; i < total; i++)
      target.appendChild(document.createTextNode("X"));
  });
  // → clever took 1 ms
</script>

Оформление

Видяхме, че различните HTML елементи показват различно поведение. Някои от тях са показани, като блокове, а други inline. Някои добавят стил, като <strong>, който удебелява съдържанието и <a>, който прави съдържанието синьо и го подчертава.

Начинът, по който един <img> таг показва изображения или <a> таг създава линк за връзка, който ще бъде последван след, като се щракне, е силно обвързан с вида на елемента. Но стилът по подразбиране свързан с даден елемент, като например цвят или подчертаване на текст, може да бъде променен от нас. Ето един пример за използване на свойството style:

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

Атрибута style може да съдържа една или повече декларации, които са свойства (като color) следвана от двуточие и стойност (като green). Когато има повече от една декларация те трябва да бъдат разделени с точка и запетая (;), както в "color: red; border: none".

Има много аспекти, които могат да бъдат повлияни от style. Например свойството display контролира дали даден елемент се показва, като block или inline.

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

Block тага ще свърши на отделен ред, тъй като блоковите елементи не се показват inline с текст около тях. Последния таг не се вижда изобщо, display: none - не позволява на елемент да се покаже на екрана. Това е начин да се скрият елементи. Това често е за предпочитане, отколкото да ги извадите от документа изцяло, защото ги прави лесни за показване отново по-късно.

JavaScript кода може директно да манипулира стила на елемент, чрез свойството style. Това свойство има обект, който има свойства за всички възможни стилове. Стойностите на тези свойства са strings, които можем да пишем за да променим конкретен аспект в стила на елемента.

<p id="para" style="color: purple">
  Pretty text
</p>

<script>
  var para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

Някои имена на свойства на стил съдържат тирета, като например font-family. Тъй като такива имена на свойства са неудобни за работа с JavaScript (ще трябва да напишем, style["font-family"]), имената за подобни свойствата в обекта style са с отстранени тирета и буквите, които ги следват са капитализирани (style.fontFamily).

CSS стилове

Системата за стайлинг на HTML се нарича CSS - Cascading Style Sheets. Style sheet е набор от правила, за това как да оформите елементи в документ. Те могат да се прилагат във вътрешността на тага <style>.

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

Cascading се отнася до факта, че могат да се комбинират множество такива правила за да се получи крайният стил за елемента. В предишния пример, стайлинга по подразбиране за <strong> тагове е font-weight: bold опакован в <style> таг, който добавя font-style и color.

Когато няколко правила определят стойността на едно и също свойство, правилото с най-висок приоритет побеждава. Така че, ако правилата включени в <style> тага са в конфликт - font-weight: normal по подразбиране текста ще бъде нормален, а не bold. Стиловете в style атрибута, които се прилагат директно към разклонението имат най-висок приоритет и винаги печелят.

Възможно е да посочите неща различни от имената на таговете с правилата на CSS. Едно правило за .abc се прилага за всички елементи с "abc" атрибут на класа. Правило за #xyz се отнася за елемент с id атрибут "xyz" (който трябва да е уникален в рамките на документа).

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* p elements, with classes a and b, and id main */
p.a.b#main {
  margin-bottom: 20px;
}

Правилото за предимство в полза на последно определеното правило важи само, когато правилата имат една и съща специфичност. Специфичност е мярка за това, колко точно се описват съвпадащите елементи, като брой и вид (таг, клас или id). Например, едно правило насочено към p.a е по-специфично от правило насочено просто към p или .a и по този начин ще има предимство.

Означението p > a {…} прилага дадените стилове за всички <a> тагове, които са преки деца на <p> таговете. По същия начин p a {…} се отнася за всички <a> тагове вътре в <p> таговете, без значение дали са преки или не преки деца.

Query selectors

Ние няма да използваме много стилове в тази книга. Въпреки, че разбирането им е от решаващо значение за програмирането във браузъра. Правилното обяснение на всички свойства, които те поддържат, както и взаимодействието между тези свойства ще отнеме две или три книги.

Основната причина за въвеждането на нотацията за селектор синтаксиса използвана в стиловете е да се определи върху кои елементи да се прилагат набори от стилове, можем да използваме този същия мини-език, като ефективен начин за намиране на DOM елементи.

Метода querySelectorAll, който се определя, като document обект и на елементите на разклоненията, взема string селектор и връща масиво-подобен обект съдържащ всички елементи, на които съответства.

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>

<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // All <p> elements
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal inside of <p>
  // → 2
  console.log(count("p > .animal")); // Direct child of <p>
  // → 1
</script>

За разлика от методи, като getElementsByTagName, обекта който връща querySelectorAll не е жив. Той няма да се промени, когато промените документа.

Метода querySelector (без All часта) работи по подобен начин. Но е полезен ако искате конкретно един елемент. Той ще върне само първия съвпадащ елемент или нула, ако няма съответствие на елементи.

Позициониране и анимиране

Свойството position на style влияе на оформлението по мощен начин. По подразбиране той е на стойност static, което означава, че елемента е в нормална позиция в документа. Когато е настроен в relative, елемента все още заема място в документа и със свойствата на style - top и left може да бъде преместен в сравнение с нормалното си място. Когато position е настроен на absolute, елемента се отстранява от нормалния поток на документа, което означава, че вече не заема място и може да се припокрива с други елементи. Също така, неговите top и left свойства могат да бъдат използвани за абсолютното му позициониране спрямо горния ляв ъгъл на най-близкия ограждащ елемент чиято position не е static или relative по отношение на документа, ако няма ограждащ елемент.

Можем да използваме това за да създадем една анимация. Следващият документ показва снимка на котка, която се движи в елипсовидна орбита.

<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  var cat = document.querySelector("img");
  var angle = 0, lastTime = null;
  function animate(time) {
    if (lastTime != null)
      angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

Картината е центрирана върху страницата и и е дадена position на relative. Ние многократно актуализираме стиловете top и left на картината за да я преместим.

Сценарият използва requestAnimationFrame за да планира animate функцията да стартира, когато браузърът е готов да опресни екрана. Самата функция animate отново призовава requestAnimationFrame да планира следващата актуализация. Когато прозореца на браузъра е активен това ще предизвика актуализациите да се случват със скорост около 60 в секунда, което има тенденцията да произведе една добре изглеждаща анимация.

Ако просто обновяваме DOM в цикъл, страницата ще замръзне и нищо няма да се появи на екрана. Браузърите не актуализират своя дисплей докато програма на JavaScript работи, нито пък позволяват някакво взаимодействие с нея. Ето защо ни трябва requestAnimationFrame, която позволява на браузъра да разбере, когато сме готови за този момент и той може да продължи напред и да прави неща, които браузърите правят, например актуализиране на екрана и реагиране на действията на потребител.

Нашата функция за анимация предава текущото време, като аргумент, който се сравнява със изминалото време (променливата lastTime), за да осигури движението на котката в милисекунди да е стабилно и движението на анимацията да е плавно. Ако тя само премества фиксираната сума за всяка стъпка, движението ще се насече, ако например друга тежка задача се изпълнява на същия компютър, ще накара функцията да спира за част от секундата.

Преместването в кръг се извършва с помощта на тригонометричните функции Math.cos и Math.sin. За тези от вас, които не са запознати с тях ще ги представим на кратко, тъй като от време на време ще се нуждаем от тях в тази книга.

Math.cos и Math.sin са полезни за намиране на точки, които са разположени в кръг около точка (0, 0) с радиус една мерна единица. И двете функции интерпретират своя аргумент, като позицията в този кръг с нула, обозначаваща точка в най-крайната дясна част на кръга, вървейки по часовниковата стрелка, докато 2π (около 6.28) не вземе разстоянието около целялата окръжност. Math.cos ни дава x-координата на точката, която съответства на дадената позиция в окръжността, а с Math.sin получаваме y- координата. Позиции (или ъгли) по-големи от 2π или по-малки от 0 са валидни - въртенето се повтаря, така че a+2π се отнася до същия ъгъл a.

Using cosine and sine to compute coordinates

Кодът на анимацията на котката подържа брояч angle за текущия ъгъл на анимацията и увеличава стойността пропорционално на изминалото време всеки път, когато animate функцията се извиква. След това можем да използваме този ъгъл за да изчислим текущата позиция на елемента в изображението. Стила top се изчислява с Math.sin умножено по 20, което е вертикалния радиус на кръга. Стила left се изчислява с Math.cos умножено по 200, така че да получим малко по-широк кръг, отколкото висок, което води до елипсовидно движение.

Имайте в предвид, че обикновено стиловете използват мерни единици. В този случай трябва да добавим "px" към числото за да кажем на браузъра, че използваме пиксели (а не сантиметри, инчове, “EMS” или други единици). Това лесно се забравя. Използване на номера, без мерни единици ще доведе до игнориране на вашия стил - освен ако въведения номер е 0, което винаги означава едно и също нещо независимо от неговата мерна единица.

Резюме

Програми на JavaScript могат да инспектират и месят в текущия документ, който браузъра показва използвайки структурата от данни, наречена DOM. Тази структура от данни представлява модел на документа на браузъра и програма на JavaScript може да го промени, за да се промени видимия документ.

DOM е организиран, като дърво, в което елементите са подредени йерархично съгласно структурата на документа. Обектите представляващи елементи имат свойства, като parentNode и childNodes, които могат да се използват за придвижване по това дърво.

Начинът, по който се показва документа може да бъде повлиян от стайлинг, както чрез пряко прикрепване на стилове към разклоненията така и чрез определяне на правила, които съответстват на определени разклонения. Има много различни свойства на стилове, като например color или display. JavaScript може да манипулира стила на елемента директно чрез своето style свойство.

Упражнения

Изграждане на таблица

В Глава 6 ние изградихме plaintext таблица. HTML прави оформлението на таблица малко по-лесно. Ето HTML таблицата изградена със следната структура:

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>country</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

Всеки ред в <table> тага съдържа <tr> таг. Вътре в тези <tr> тагове можем да сложим клетъчни елементи: за клетки за заглавия (<th>) или за нормални клетки (<td>).

Същата структура от данни, които използвахме в Глава 6 е отново на разположение в MOUNTAINS променливата в пясъчника или може да бъде изтеглена от сайта.

Напишете функция buildTable, която по даден масив от обекти, които имат еднакъв набор от свойства, изгражда структура на DOM представляваща една таблица. Таблицата трябва да има header (заглавен) ред с имена увити в <th> елементи и трябва да има един следващ ред от обекти на масива с техните стойности увити в <td> елементи.

Функцията Object.keys, която връща масив съдържащ имената на свойствата, които един обект има, най-вероятно ще ви е от полза тука.

След като имате основните клетки стоящи от дясно, подравнете клетките съдържащи числа чрез създаване на style.textAlign свойство за "right" (дясно).

<style>
  /* Defines a cleaner look for tables */
  table  { border-collapse: collapse; }
  td, th { border: 1px solid black; padding: 3px 8px; }
  th     { text-align: left; }
</style>

<script>
  function buildTable(data) {
    // Your code here.
  }

  document.body.appendChild(buildTable(MOUNTAINS));
</script>

Използвайте document.createElement за да създадете разклонение на нов елемент, document.createTextNode за да създадете текстово разклонение, както и appendChild метода за да сложите едни разклонения в други разклонения.

Трябва да минете с цикъл над ключовите имена веднъж за да попълните най-горния ред и след това отново за всеки обект в масива за да изградите редовете с данни.

Не забравяйте да върнете затварящия <table> елемент в края на функцията.

Elements by tag name

Методът getElementsByTagName връща всички дъщерни елементи с даденото име на тага. Приложете своя собствена версия на това, като редовна nonmethod функция, която взема едно разклонение и string (името на тага), като аргументи и връща масив съдържащ всички елементи-деца на разклонението с даденото име на тага.

За да намерите името на тага на даден елемент, използвайте неговото tagName свойство. Но имайте в предвид, че това ще върне името на тага във главни букви. Използвайте toLowerCase или toUpperCase методи на string за да компенсирате това.

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>

<script>
  function byTagName(node, tagName) {
    // Your code here.
  }

  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  var para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

Решението най-лесно се изразява с рекурсивна функция, подобна на talksAbout функцията определена по-рано в тази глава.

Можете да извикате byTagname рекурсивно, слепвайки получените масиви за производството на изхода. За по-ефективен подход, дефинирайте една вътрешна функция, която се самоизвиква рекурсивно и има достъп до променливата на масива, дефинирана във външната функция, в която той може да добави съвпадащите елементи, които намери. Не забравяйте да извикате вътрешната функция веднъж от външната функция.

Рекурсивната функция трябва да провери вида на разклонението. Тука ние се интересуваме само от node type - 1 (document.ELEMENT_NODE). За такива разклонения ни трябва цикъл, с който да минем над децата и за всяко дете да видим дали то съответства на заявката, като същевременно правим рекурсивно извикване върху него, за да инспектираме неговите собствени деца.

Шапката на котката

Разширете анимацията на котката дефинирана по-рано, така че котката и шапката (<img src="img/hat.png">) да обикалят в орбита от противоположните страни на елипсата.

Или направи шапка върху главата на котката. Или промени анимацията по някакъв друг интересен начин.

За да бъдат по-лесно позиционирани множество обекти, е може би добра идея да се премине към абсолютно позициониране. Това означава, че top и left частта са relative спрямо горния ляв ъгъл на документа. За да избегнете използването на отрицателни координати, можете просто да добавите фиксиран брой пиксели към стойностите на позициите.

<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
  var cat = document.querySelector("#cat");
  var hat = document.querySelector("#hat");
  // Your code here.
</script>