forked from marijnh/Eloquent-JavaScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path13_dom.txt
1085 lines (880 loc) · 38.1 KB
/
13_dom.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
:chap_num: 13
:prev_link: 12_browser
:next_link: 14_event
:load_files: ["code/mountains.js", "code/chapter/13_dom.js"]
= The Document Object Model =
When you open a web page in your browser, it retrieves the page's
HTML text and parses it, much like the way our parser from
link:11_language.html#parsing[Chapter 11] parsed programs. The browser builds up a
model of the document's structure, and then uses this model to draw the page
on the screen.
One of the toys that a JavaScript program has available in its sandbox
is this representation of the document. You can read from it, and also
change it. It acts as a _live_ data structure: when it is modified, the
page on the screen is updated to reflect the changes.
== Document structure ==
You can imagine an HTML document as a nested set of boxes. Tags like
`<body>` and `</body>` enclose other tags, which in turn contain other
tags, or text. Here's the example document from last chapter:
[sandbox="homepage"]
[source,text/html]
----
<!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>
----
This page has the following structure:
image::img/html-boxes.svg[alt="HTML document as nested boxes",width="7cm"]
The data structure the browser uses to represent the document follows
this shape. For each box, there is an object, which we can interact
with to find out things like what HTML tag it represents, and which
boxes and text it contains. This representation is called the
_Document Object Model_, DOM for short.
The global variable `document` gives us access to these objects. Its
`documentElement` property refers to the object representing the
`<html>` tag. It also provides properties `head` and `body`, holding
the objects for those elements.
== Trees ==
Think back to the syntax trees from link:11_language.html#parsing[Chapter 11] for a moment. Their
structure is strikingly similar to the structure of a browser's
document. Each _node_ may refer to other nodes, _children_, which may
have their own children. This shape is typical of nested structures
where elements can contain sub-elements that are similar to
themselves.
We call a data structure a _tree_ when it has a branching structure,
no cycles (a node may not contain itself, directly or indirectly), and
has a single, well-defined “root”. In the case of the DOM,
`document.documentElement` serves as the root.
Trees come up a lot in computer science. Apart from representing
recursive structures like HTML documents or programs, they are also
often used to maintain sorted sets of data, because elements can
typically be found or inserted more efficiently in a sorted tree than
in a sorted flat array.
A typical tree has different kinds of nodes. The syntax tree for link:11_language.html#language[the Egg language] had
variables, values, and application nodes. Application nodes always had
children, whereas variables and values were _leaves_, nodes without children.
The same goes for the DOM. Nodes for regular _elements_, which
represent HTML tags, determine the structure of
the document. These can have child nodes. An example of such a node is
`document.body`. Some of these children can be leaf nodes, such as
pieces of text or comments (which are written between `<!--` and `-->`
in HTML).
Each DOM node object has a `nodeType` property, which contains a
numeric code that identifies the type of node. Regular nodes have the
value 1, which is also defined as the constant property
`document.ELEMENT_NODE`. Text nodes, representing a section of text
in the document, have the value 3 (`document.TEXT_NODE`). Comments get the value
8 (`document.COMMENT_NODE`).
So another way to visualize our document tree is:
image::img/html-tree.svg[alt="HTML document as a tree",width="8cm"]
The leaves are text nodes, and the arrows indicate
parent-child relationships between nodes.
[[standard]]
== The standard ==
Using cryptic numeric codes to represent node types is not a very
JavaScript-like thing to do. Further on in this chapter, we'll see
that other parts of the DOM interface also feel cumbersome and alien.
The reason for this is that the DOM wasn't designed for just
JavaScript, but rather tries to define a language-neutral interface
that can be used in other systems as well—not just HTML, but also
XML, which is a generic data format with an HTML-like syntax.
This is unfortunate. Standards are often useful. But in this case, the
advantage (cross-language consistency) isn't all that compelling. Having
an interface that is properly integrated with the language you are using
will save you more time than having a familiar interface across
languages.
As an example of such poor integration, consider the `childNodes`
property that element nodes in the DOM have. This property holds an
array-like object, with a `length` property and properties labeled by
numbers to access the child nodes. But it is an instance of
the `NodeList` type, not a real array, so it does not have methods
like `slice` and `forEach`.
Then there are issues that are simply the result of poor design. For
example, there is no way to create a new node and immediately add
children or attributes to it. Instead, you have to first create it,
then add the children one by one, and set the attributes one by one.
Code that interacts heavily with the DOM tends to get very long,
repetitive, and ugly.
But none of these flaws are fatal, since JavaScript allows us to create our
own abstractions. It is easy to write some helper functions that allow
you to express the operations you are performing in a clearer and
shorter way. In fact, many libraries intended for browser programming
come with such tools.
== Moving through the tree ==
DOM nodes contain a wealth of links to other nearby nodes. The
following diagram tries to illustrate these.
image::img/html-links.svg[alt="Links between DOM nodes",width="6cm"]
Although the diagram only shows one link of each type, every node has
a `parentNode` property that points to its containing node. Likewise,
every element node (node type 1) has a `childNodes` property that
points to an array-like object holding its children.
In theory, you could move anywhere in the tree using just these
parent and child links. But JavaScript also gives you access to
a number of additional convenience links. The `firstChild`
and `lastChild` properties point to the first and last child element,
or have the value `null` for nodes without children. Similarly,
`previousSibling` and `nextSibling` point to adjacent nodes, nodes
with the same parent that appear immediately before or after the node
itself. For a first child, `previousSibling` will be null, and for a
last child, `nextSibling` is null.
When dealing with a nested data structure like this,
recursive functions are often useful. The one
below scans a document for text nodes containing a given string, and
returns `true` when it has found one.
[[talksAbout]]
[sandbox="homepage"]
[source,javascript]
----
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
----
The `nodeValue` property of a text node refers to the string of text
that it represents.
== Finding elements ==
Navigating these links among parents, children, and siblings is often
useful, as in the function above, which runs through the whole
document. But if we want to find a specific node in the document,
reaching it by starting at `document.body` and blindly following a hard-coded
path of links is a bad idea. Doing so bakes assumptions into our program
about the precise structure of the document—a structure we might want to
change later. Another complicating factor is that text
nodes are created even for the whitespace
between nodes. The example document's body tag does not have just
three children (`<h1>` and two `<p>`’s), but actually has seven: those
three, plus the spaces before, after, and between them).
So if we want to get the `href` attribute of the link in that
document, we don't want to say something like “get the second child of
the sixth child of the document body”. It'd be better if we could say
“get the first link in the document”. And we can.
[sandbox="homepage"]
[source,javascript]
----
var link = document.body.getElementsByTagName("a")[0];
console.log(link.href);
----
All element nodes have a `getElementsByTagName` method, which collects
all elements with the given tag name that are descendants (direct or
indirect children) of the given node, and returns them as an
array-like object.
To find a specific _single_ node, you can give it an `id` attribute,
and use `document.getElementById` instead.
[source,text/html]
----
<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>
----
A third, similar method is `getElementsByClassName`, which, like
`getElementsByTagName`, searches through the contents of an element
node, and retrieves all elements that have the given string in their
`class` attribute.
== Changing the document ==
Almost everything about the DOM data structure can be changed. Element
nodes have a number of methods that can be used to change their
content. The `removeChild` method removes the given child node from
the document. To add a child, we can use `appendChild`, which puts it
at the end of the list of children, or `insertBefore`, which inserts
the node given as first argument before the node given as second
argument.
[source,text/html]
----
<p>One</p>
<p>Two</p>
<p>Three</p>
<script>
var paragraphs = document.body.getElementsByTagName("p");
document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>
----
A node can only exist in the document in one place. Thus,
inserting paragraph “Three” in front of paragraph “One” will first
remove it from the end of the document, and then insert it at the
front, resulting in “Three/One/Two”. All operations that insert a node
somewhere will, as a side effect, cause it to be removed from its
current position (if it has one).
The `replaceChild` method is used to replace a child node with another
one. It takes as arguments two nodes: a new node, and the node to
be replaced. The replaced node must be a child of the element the method is
called on. Note that both `replaceChild` and `insertBefore` expect the
_new_ node as their first argument.
== Creating nodes ==
In the next example, we want to write a script that replaces all
images (`<img>` tags) in the document with the text held in their
`alt` attribute, which specifies an alternative
textual representation of the image.
This involves not only removing the images, but adding a new text node
to replace them. For this, we use the `document.createTextNode`
method.
[source,text/html]
----
<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>
----
Given a string, `createTextNode` gives us a type 3 DOM node (a text node), which we
can insert into the document to make it show up on the screen.
The loop that goes over the images starts at the end of the list
of nodes. This is necessary because the node list returned by a method
like `getElementsByTagName` (or a property like `childNodes`) is
__live__—that is, it is updated as the document changes. If we started from the
front, removing the first image would cause the list to lose its first
element, so that the second time the loop repeats, where `i` is one,
it would stop, because the length of the collection is now also one.
If you want a _solid_ collection of nodes, as opposed to a live one,
you can convert the collection to a real array by calling the array
`slice` method on it.
[source,javascript]
----
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
----
To create regular element nodes (type 1), you can use the
`document.createElement` method. This method takes a tag name and
returns a new empty node of the given type.
[[elt]]
The following example defines a utility `elt`, which creates an
element node, and treats the rest of its arguments as children to that
node. This function is then used to add a simple attribution to a
quote.
[source,text/html]
----
<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>
----
ifdef::tex_target[]
image::img/blockquote.png[alt="A blockquote with attribution",width="8cm"]
endif::tex_target[]
== Attributes ==
Some element attributes, such as `href` for links, can be accessed
through a property of the same name on the element's DOM
object. This is the case for a limited set of commonly used standard
attributes.
But HTML allows you to set any attribute you want on nodes. This can
be useful, as it allows you to store extra information for your
scripts in a document. If you make up your own attribute names,
though, such attributes will not be present as a property on the
element's node. Instead, you'll have to use the `getAttribute` and
`setAttribute` methods to work with them.
[source,text/html]
----
<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>
----
I recommended prefixing the names of such made-up attributes with
`data-`, to ensure that they do not conflict with any other
attributes.
As a simple example, we'll write a “syntax highlighter” that looks for
`<pre>` tags (“pre-formatted”, used for code and similar plain text)
with a `data-language` attribute, and crudely tries to highlight the
keywords for that language.
// include_code
[sandbox="highlight"]
[source,javascript]
----
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));
}
----
The function `highlightCode` takes a `<pre>` node and a regular
expression (with the “global” option turned on) that matches the
keywords of the programming language that the element contains.
The `textContent` property is used to get all the text in the node, and
is then set to an empty string, which has the effect of emptying the
node. We loop over all matches of the keyword expression, appending
the text _between_ them as regular text nodes, and the text matched as
text nodes wrapped in `<strong>` (bold) elements.
We can automatically highlight all programs on the page by looping
over all the `<pre>` elements that have a `data-language` attribute, and
calling `highlightCode` on each one with the correct regular expression
for the language.
// include_code
[sandbox="highlight"]
[source,javascript]
----
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]);
}
}
----
For example:
[sandbox="highlight"]
[source,text/html]
----
<p>Here it is, the identity function:</p>
<pre data-language="javascript">
function id(x) { return x; }
</pre>
<script>highlightAllCode();</script>
----
ifdef::tex_target[]
image::img/highlighted.png[alt="A highlighted piece of code",width="4.8cm"]
endif::tex_target[]
There is one commonly used attribute, `class`, which is a reserved
word in the JavaScript language. For historical reasons—some old
JavaScript implementations could not handle property names that matched
keywords or reserved words—the property used to access this attribute
is called `className`. You can also access it with the
`getAttribute` and `setAttribute` methods.
== Layout ==
You might have noticed that different types of elements are laid out
differently. Some, such as paragraphs (`<p>`) or headings (`<h1>`),
take up the whole width of the document, and are rendered on separate
lines. These are called _block_ elements. Others, such as links
(`<a>`) or the `<strong>` element used in the example above, are
rendered on the same line with their surrounding text. Such elements
are called _inline_ elements.
For any given document, browser are able to compute a
layout, which gives each element a size and position based on its type
and content. This layout is then used to actually draw the document.
The size and position of an element can be accessed from JavaScript.
The `offsetWidth` and `offsetHeight` properties give you the space the
element takes up in _pixels_. A pixel is the basic unit of measurement in the
browser, and typically corresponds to the smallest dot that your screen
can display. Similarly, `clientWidth` and `clientHeight` give you the
size of the space _inside_ the element, ignoring border width.
[source,text/html]
----
<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>
----
ifdef::tex_target[]
image::img/boxed-in.png[alt="A paragraph with a border",width="8cm"]
endif::tex_target[]
[[boundingRect]]
The most effective way to find the precise position of an element on
the screen is the `getBoundingClientRect` method. It returns an object
with `top`, `bottom`, `left`, and `right` properties, indicating the
pixel positions of the sides of the element relative to the top left
of the _screen_. If you want them relative to the whole document, you
must add the current scroll position, found under the global
`pageXOffset` and `pageYOffset` variables.
Laying out a document can be quite a lot of work. In the interest of
speed, browser engines do not immediately re-layout a document every
time it is changed, but rather wait as long as they can. When a
JavaScript program that changed the document finishes running, the
browser will have to compute a new layout in order to display the
changed document on the screen. When a program _asks_ for the position or size of something
by reading properties like `offsetHeight` or calling
`getBoundingClientRect`, providing correct information also requires
computing a layout.
A program that repeatedly alternates between reading DOM layout
information and changing the DOM forces a lot of layouts to
happen, and will consequently run really slowly.
The following code shows an example of this.
It contains two different programs that build up a line of
“X” characters 2000 pixels wide, and measures the time each one takes.
// test: nonumbers
[source,text/html]
----
<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>
----
== Styling ==
We have seen that of different HTML elements display different
behavior. Some are displayed as blocks, others inline. Some add
styling, like `<strong>` making its content bold, and `<a>` making it
blue and underlining it.
The way an `<img>` tag shows an image or an `<a>` tag causes
a link to be followed when it is clicked are strongly tied to the element
type. But the default styling associated with an element, such as the text color
or underline, can be changed by us. For example by using the `style` property.
[source,text/html]
----
<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>
----
ifdef::tex_target[]
image::img/colored-links.png[alt="A normal and a green link",width="2.2cm"]
endif::tex_target[]
(((colon character)))A style attribute may contain one or more _declarations_, which are a
property (such as `color`) followed by a colon and a value (such as
`green`). When there are more than one declaration, they must be
separated by semicolons. For example, “`color: red; border: none`”.
There are a lot aspects that can be influenced with styling. For
example, the `display` property controls whether an element is displayed as a block or inline
element.
[source,text/html]
----
This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.
----
ifdef::tex_target[]
image::img/display.png[alt="Different display styles",width="4cm"]
endif::tex_target[]
The `block` tag will end up on its own line, since block elements are
not displayed inline with the text around them. The last tag is not
displayed at all—`display: none` prevents an element from showing up
on the screen. This is a way to hide elements, and it is often
preferable to removing them from the document entirely—it makes
it easy to reveal them again at a later time.
JavaScript code can directly manipulate the style of an element
through the node's `style` property. This property holds an
object that has properties for all possible style properties. The
values of these properties are strings, which we can write to in order
to change a particular aspect of the element's style.
[source,text/html]
----
<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>
----
Some style property names contain dashes, like `font-family`.
Because such property names are awkward to work with in JavaScript
(you'd have to say `style["font-family"]`), the property names in the
`style` object for such properties have their dashes removed, and the
letter that follows them capitalized (`style.fontFamily`).
== Cascading styles ==
The styling system for HTML is called CSS, for _Cascading Style
Sheets_. A _style sheet_ is a set of rules for how to style elements
in the document. It can be given inside a `<style>` tag.
[source,text/html]
----
<style>
strong {
font-style: italic;
color: grey;
}
</style>
<p>Now <strong>strong text</strong> is italic and grey.</p>
----
The _cascading_ in the name refers to the fact that multiple such
rules get combined to produce the final style for an element. In the
example above, the default styling for `<strong>` tags, which gives
them `font-weight: bold`, is overlaid by the rule in the `<style>`
tag, which adds `font-style` and `color`.
When multiple rules define a value for the same property,
the most recently
read rule gets a higher precedence and wins out. So
if the rule in the `<style>` tag included `font-weight: normal`,
conflicting with the default `font-weight` rule,
text would be normal, _not_ bold. Styles in a `style` attribute applied
directly to the node have the highest precedence, and always win.
It is possible to target things other than tag names in CSS rules. A
rule for `.abc` applies to all elements with `"abc"` in their class
attribute. A rule for `#xyz` applies to the element with an `id`
attribute of `"xyz"` (which should be unique, within the document).
[source,text/css]
----
.subtle {
color: grey;
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;
}
----
The precedence rule favoring the most recently defined rule only holds
when the rules have the same _specificity_. A rule's specificity is a
measure of how precisely it describes matching elements, determined by
the number and kind (tag, class, or id) of element aspects it
requires. For example, `p.a` is more specific than just `p` or just
`.a`, so a rule defined as such would take precedence to them.
The notation `p > a {…}` applies the given styles to all `<a>` tags
that are direct children of `<p>` tags. Similarly, `p a {…}` applies
to all `<a>` tags inside `<p>` tags, whether they are direct or
indirect children.
== Query selectors ==
We won't be using style sheets all that much in this book.
Although understanding them is crucial to programming in the browser,
properly explaining all properties they support, and the interaction
among those properties, would take two or three books in itself.
The main reason I introduced _selector_ syntax—the notation used in
style sheets to determine which elements a set of styles apply to—is
that we can use this same mini-language as an effective way to
find DOM elements.
The `querySelectorAll` method, which is defined both on the `document` object
and on element nodes, takes a selector string, and returns an
array-like object containing all the elements that it matches.
[source,text/html]
----
<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>
----
The `querySelector` method (without the `All` part) works in a similar
way. This one is useful if you want a specific, single element. It
will return only the first matching element, or null if no elements
match.
[[animation]]
== Positioning and animating ==
The `position` style property influences layout in a powerful
way.
By default it has a value of `static`, meaning the element sits in its
normal place in the document. When it is set to `relative`, the
element still takes up space in the document, but now the `top` and `left`
style properties can be used to move it relative to its normal place.
When `position` is set to `absolute` the element is removed from the
normal document flow—that is, it no longer takes up space, and may overlap
with other elements—and its `top` and `left` properties can be used to
absolutely position it relative to the top left corner of the nearest
enclosing element whose `position` property isn't `static`, or the document
if no such enclosing element exists.
We can use this to create an animation. The document below displays a
picture of a cat that floats around in an ellipse.
[source,text/html]
----
<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>
----
ifdef::tex_target[]
image::img/cat-animation.png[alt="A moving cat head",width="8cm"]
endif::tex_target[]
First, the picture is centered on the page, and given a `position` of
`relative`. We'll repeatedly update that picture's `top` and `left`
styles in order to move it.
[[animationFrame]]
The script uses `requestAnimationFrame` to schedule the
`animate` function to run whenever the browser is ready to repaint the screen.
The `animate` function itself again calls `requestAnimationFrame` to
schedule the next update. When the browser window (or tab) is active,
this will cause updates to happen at a rate of about 60 per second,
which tends to produce a good-looking animation.
If we just updated the DOM in a loop, the page would freeze and
nothing would show up on the screen. Browsers do not update their
display while a
JavaScript program is running, nor do they allow any interaction with the page. This is why we need
++requestAnimationFrame++—it lets the browser know that we are done
for now, and it can go ahead and do the things that browsers do,
such as updating the screen and responding to user actions.
Our animation function is passed the current time as argument, which
it compares to the time it saw before (the `lastTime` variable) to
ensure the motion of the cat per millisecond is stable, and the
animation moves smoothly. If it just moved a fixed amount per step, the
motion would stutter if, for example, another heavy task
running on the same computer were to prevent the function from running for a
fraction of a second.
[[sin_cos]]
Moving in circles is done using the trigonometry functions `Math.cos`
and `Math.sin`. For those of you who aren't familiar with these, I'll
briefly introduce them, since we will occasionally need them in this
book.
`Math.cos` and `Math.sin` are useful for finding points that lie on a
circle around point (0,0), with each point exactly one unit away
from (0,0). Both functions interpret their argument as the position on this
circle, with zero denoting the point on the far right of the circle,
going clockwise until 2π (about 6.28) has taken us around the whole
circle. `Math.cos` tells you the x coordinate of the point that
corresponds to the given position around the circle, while `Math.sin` yields the y coordinate.
Positions (or angles) higher than 2π or below 0 are valid—the rotation
repeats, so that _a_+2π refers to the same angle as _a_.
image::img/cos_sin.svg[alt="Using cosine and sine to compute coordinates",width="6cm"]
The cat animation code keeps a counter, `angle`, for the current angle of the
animation, and increments it in proportion to the elapsed
time every time the `animate` function is called. It can then use this
angle to compute the current position of the image element. The `top`
style is computed with `Math.sin`, and multipled by 20, which is the
vertical radius of our circle. The `left` style is based on
`Math.cos`, and multipled by 200, so that the circle is much wider
than it is high, resulting in an elliptic motion.
Note that styles usually need _units_. In this case, we have to append
`"px"` to the number to tell the browser we are counting in pixels (as
opposed to centimeters, “ems”, or other units). This is easy to
forget. Using numbers without units will result in your style being
ignored—unless the number is 0, which always means the same, regardless
of its unit.
== Summary ==
JavaScript programs may inspect and interfere with the current
document that a browser is displaying through a data structure called
the DOM. This data structure represents the browser's model of the
document, and a JavaScript program can modify it to change the visible
document.
The DOM is organized like a tree, in which elements are arranged
hierarchically according to the structure of the document. The objects
representing elements have properties like `parentNode` and
`childNodes`, which can be used to navigate through this tree.
The way a document is displayed can be influenced by _styling_, both
by attaching styles to nodes directly, and by defining rules that
match certain nodes. There are many different style properties, such as
`color` or `display`. JavaScript can manipulate an
element's style directly through its `style` property.
== Exercises ==
[[exercise_table]]
=== Build a table ===
We built plain-text tables in link:06_object.html#tables[Chapter 6].
HTML makes laying out tables quite a bit easier. An HTML table is
built with the following tag structure:
[source,text/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>
----
For each _row_, the `<table>` tag contains a `<tr>` tag. Inside of
these, we can put cell elements: either heading cells (`<th>`) or
regular cells (`<td>`).
The same source data that was used in
link:06_object.html#mountains[Chapter 6] is again available in the
`MOUNTAINS` variable in the sandbox, and also
http://eloquentjavascript.net/code/mountains.js[downloadable] from the
list of data sets on the website(!tex (http://eloquentjavascript.net/code[_eloquentjavascript.net/code_])!).
Write a function `buildTable` that, given an array of objects that
all have the same set of properties, builds up a DOM structure
representing a table. The table should have a header row with the
property names wrapped in `<th>`
elements, and one subsequent row per object in the array, with
its property values in `<td>` elements.
The `Object.keys` function, which returns an array containing the
property names that an object has, will probably be helpful here.
Once you have the basics working, right-align cells containing numbers
by setting their `style.textAlign` property to `"right"`.
ifdef::html_target[]
// test: no
[source,text/html]
----
<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>
----
endif::html_target[]
!!solution!!
Use `document.createElement` to create new element nodes,
`document.createTextNode` to create text nodes, and the `appendChild`
method to put nodes into other nodes.
You should loop over the key names once to fill in the top row, and
then again for each object in the array to construct the data
rows.
Don't forget to return the enclosing `<table>` element at the end of
the function.
!!solution!!
=== Elements by tag name ===
The `getElementsByTagName` method returns all child elements with a
given tag name. Implement your own version of it, as a regular
non-method function, which takes a node and a string (the tag name) as
arguments, and returns an array containing all descendant