forked from marijnh/Eloquent-JavaScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path14_event.txt
1014 lines (849 loc) · 35 KB
/
14_event.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: 14
:prev_link: 13_dom
:next_link: 15_game
= Handling Events =
[quote,Marcus Aurelius,Meditations]
____
You have power over your mind—not outside events. Realize this, and
you will find strength.
____
Some of the things that a program works with, such as direct user
input, happen at unpredictable times, in an unpredictable order. This
requires a different approach to control than the one we have used so
far.
== Event handlers ==
Imagine an interface where the only way to find out whether a button
is pressed is to read the current state of that button. In order to be
able to react to that button, you would have to constantly read the
button's state, so that you'd catch it before it was released again.
It would be dangerous to perform other computations that took a while,
since you might miss a button press.
That is how things were done on _very_ primitive machines. A step up
would be for the hardware or the operating system to notice the button
press, and put it in a queue somewhere. Our program can then
periodically check whether something has appeared in this queue, and
react to what it finds there.
Of course, it has to remember to look at the queue, and to do it
often, because any time elapsed between the button being pressed and
our program getting around to seeing if a new event came in will cause
the software to feel unresponsive. This approach is called _polling_.
Most programmers avoid it whenever possible.
A better mechanism is for the underlying system to immediately give
our code a chance to react to events as they occur. Browsers do this,
by allowing us to register functions as _handlers_ for specific
events.
[source,text/html]
----
<p>Click on this document to activate the handler.</p>
<script>
addEventListener("click", function() {
console.log("You clicked!");
});
</script>
----
The `addEventListener` function registers its second argument to be
called whenever the event described by its first argument occurs.
== Events and DOM nodes ==
Each browser event handler is registered in a context. When you call
`addEventListener` as above, you are calling it as a method on the
whole window, because in the browser the global scope is equivalent to
the `window` object. Every DOM element has an `addEventListener` method,
which allows you to listen specifically on that element.
[source,text/html]
----
<button>Click me</button>
<p>No handler here.</p>
<script>
var button = document.querySelector("button");
button.addEventListener("click", function() {
console.log("Button clicked.");
});
</script>
----
The example attaches a handler to the button node, and thus only
clicks on the button cause that handler to run.
An `onclick` attribute put directly on a node has a similar effect.
But a node only has one `onclick` attribute, so you can only register
one handler per node that way. The `addEventListener` method allow any
number of handlers to be added, so that you won't accidentally replace
a handler registered by some other code.
The `removeEventListener` method, called with the same type of
arguments as `addEventListener` removes a handler again.
[source,text/html]
----
<button>Act-once button</button>
<script>
var button = document.querySelector("button");
function once() {
console.log("Done.");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>
----
In order to be able to unregister it, we had to give our handler
function a name (`once`), so that we can pass the same value to
`removeEventListener` that we passed to `addEventListener`.
== Event objects ==
Though we have ignored it in the examples above, event handler
functions are actually passed an argument, the event object. This
object gives us additional information about the event. For example,
if we want to know _which_ mouse button was pressed, we can look at
this object's `button` property.
[source,text/html]
----
<button>Click me any way you want</button>
<script>
var button = document.querySelector("button");
button.addEventListener("mousedown", function(event) {
if (event.button == 0)
console.log("Left button");
else if (event.button == 1)
console.log("Middle button");
else if (event.button == 2)
console.log("Right button");
});
</script>
----
The information stored in an event object differs per type of event.
We'll discuss various types further on in this chapter. The object's
`type` property always holds a string identifying the event (for
example `"click"` or `"mousedown"`).
== Bubbling ==
Event handlers registered on nodes with children will also receive
events that happen in the children. If a button inside a paragraph is
clicked, event handlers on the paragraph will also receive the click
event.
But if both the paragraph and the button have a handler, the more
specific handler—the one on the button—gets to go first. Conceptually,
the event _bubbles_ outwards, from the node where it happened, to that
node's parent node, and on up to the root of the document. And
finally, handlers registered on the whole window get a chance to
respond to the event.
At any point, an event handler can call the `stopPropagation` method
on the event object to prevent handlers “further up” from receiving
the event. This can be useful when, for example, you have a button
inside another clickable element, and you don't want clicks on the
button to also activate the outer element's click behavior.
The example below registers mousedown handler on both a button and the
paragraph around it. When clicked with the right mouse button, the
handler for the button calls `stopPropagation`, which will prevent the
handler on the paragraph from running. When the button is clicked with
another mouse button, both handlers will run.
[source,text/html]
----
<p>A paragraph with a <button>button</button>.</p>
<script>
var para = document.querySelector("p");
var button = document.querySelector("button");
para.addEventListener("mousedown", function() {
console.log("Handler for paragraph.");
});
button.addEventListener("mousedown", function(event) {
console.log("Handler for button.");
if (event.button == 2)
event.stopPropagation();
});
</script>
----
Event objects have a `target` property that refers to the node where
they originate. You can also use this to ensure that you are not
accidentally handling something that bubbled up from a node you do not
want to handle. It is also possible to use the `target` property to
cast a wide net for a specific type of event. For example, if you
have a node containing a long list of buttons and you want to handle
clicks on these buttons, it may be more inconvenient to register a
single click handler on the outer node, and have it figure out whether
a button was clicked, than to register individual handlers on all of
the buttons.
[source,text/html]
----
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", function(event) {
if (event.target.nodeName == "BUTTON")
console.log("Clicked", event.target.textContent);
});
</script>
----
== Default actions ==
Many events have a default action associated with them by the browser.
If you click a link, you will be taken to the link's target. If you
press the down arrow, the browser will scroll the page down. A right
click opens a context menu. And so on.
For most types of events, the JavaScript event handlers are called
_before_ the default behavior is performed. When the handler takes
care of handling the event, and does not want the normal behavior to
also happen, it can call the `preventDefault` method on the event
object.
This can be used to implement your own keyboard shortcuts or context
menu. It can also be used to obnoxiously interfere with the behavior
that users expect. For example, here is a link that can not be
followed:
[source,text/html]
----
<a href="https://developer.mozilla.org/">MDN</a>
<script>
var link = document.querySelector("a");
link.addEventListener("click", function(event) {
console.log("Nope.");
event.preventDefault();
});
</script>
----
Try not to do such things unless you have a really good reason to. For
people using your page, it can be very unpleasant when expected
behavior is broken.
Depending on the browser, some events can not be intercepted. On
Chrome, for example, keyboard shortcuts to close the current tab
(Control-W or Command-W) can not be handled by JavaScript.
== Key events ==
When a key on the keyboard is pressed down, your browser fires a
`"keydown"` event. When it is released again, a `"keyup"` event is
fired.
[source,text/html]
[focus="yes"]
----
<p>This page turns violet when you hold the V key.</p>
<script>
addEventListener("keydown", function(event) {
if (event.keyCode == 86)
document.body.style.background = "violet";
});
addEventListener("keyup", function(event) {
if (event.keyCode == 86)
document.body.style.background = "";
});
</script>
----
Despite its name, `"keydown"` is not only fired when the key is
physically pushed down. When a key is pressed and held, the event is
fired again every time the key _repeats_. Sometimes, for example when
you want to increase the acceleration of your game character when an
arrow key is pressed, and decrease it again when the key is released,
you have to be careful not to increase it again every time the key
repeats, or you'd end up with unintentionally huge values.
The example above looked at the `keyCode` property of the event
object. This is the way we can identify which key is being pressed or
released. Unfortunately, it holds a number, and translating that
number to an actual key is not always obvious.
For letter and number keys, the associated key code will be the
Unicode character code associated with the (upper case) letter printed
on the key. The `charCodeAt` method on strings gives us a way to find
this code:
[source,javascript]
----
console.log("Violet".charCodeAt(0));
// → 86
console.log("1".charCodeAt(0));
// → 49
----
Other keys have less predictable key codes. The best way to find the
codes you need is usually by experiment—register a key event handler
that logs the key codes it gets, and press the key you are interested
in.
Modifier keys like shift, control, alt, and meta (“command” on Mac)
generate key events just like normal keys. But when looking for key
combinations, you can also also find out whether these keys are held
down by looking at the `shiftKey`, `ctrlKey`, `altKey`, and `metaKey`
properties of keyboard (and mouse) events.
[source,text/html]
[focus="yes"]
----
<p>Press Control-Space to continue.</p>
<script>
addEventListener("keydown", function(event) {
if (event.keyCode == 32 && event.ctrlKey)
console.log("Continuing!");
});
</script>
----
The `"keydown"` and `"keyup"` events give you information about the
physical key that is being hit. When you are interested in text that
is being typed, deriving that from key codes is awkward. Instead,
there exists another event, `"keypress"`, for this purpose. It is
fired right after `"keydown"` (and repeated along with `"keydown"`
when the key is held), but only for keys that produce character input.
The `charCode` property in the event object contains a code that can
be interpreted as a Unicode character code. The `String.fromCharCode`
function can be used to turn this code into an actual
(single-character) string.
[source,text/html]
[focus="yes"]
----
<p>Focus this page and type something.</p>
<script>
addEventListener("keypress", function(event) {
console.log(String.fromCharCode(event.charCode));
});
</script>
----
The DOM node where key events originate depends on the element that is
currently focused. Normal nodes can not be focused (unless you give
them a `tabindex` attribute), but things like links, buttons, and form
fields can. We'll come back to form fields in Chapter 18. When nothing
in particular is focused, `document.body` acts as the target node of
key events.
== Mouse clicks ==
Clicking a mouse button also causes a number of events to be fired.
The `"mousedown"` and `"mouseup"` events are similar to `"keydown"`
and `"keyup"`, fired when the button is pressed and released. These
will happen on the DOM nodes that are immediately below the mouse
pointer when the event occurs.
After the `"mouseup"` event, a `"click"` event is fired on the most
specific node that contained both the press and the release of the
button. For example, if I press down the mouse button on one
paragraph, and then move the pointer to another paragraph and release
the button, the `"click"` event will happen on the element that
contains both those paragraphs.
If two clicks quickly follow each other, a `"dblclick"` (double click)
event is also fired, after the second click event.
To get precise information about the place where a mouse event
happened, you can look at its `pageX` and `pageY` properties, which
contain the event's coordinates (in pixels) relative to the top left
corner of the document.
The following implements a primitive drawing program. Every time you
click on the document, it adds a dot under your mouse pointer.
[source,text/html]
----
<style>
body {
height: 200px;
background: beige;
}
.dot {
height: 8px; width: 8px;
border-radius: 4px; /* rounds corners */
background: blue;
position: absolute;
}
</style>
<script>
addEventListener("click", function(event) {
var dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (event.pageX - 4) + "px";
dot.style.top = (event.pageY - 4) + "px";
document.body.appendChild(dot);
});
</script>
----
The `clientX` and `clientY` properties are similar to `pageX` and
`pageY`, but relative to the part of the document that is currently
scrolled into view. These can be useful when comparing mouse
coordinates with the coordinates returned by `getBoundingClientRect`,
which yields the same type of coordinates.
== Mouse motion ==
Every time the mouse pointer moves, a `"mousemove"` event fires. This
event can be used to track the position of the mouse. A common
situation in which this is useful is when implementing some form of
mouse-dragging functionality.
As an example, the program below displays a bar, and sets up
event handlers so that dragging to the left or right on this bar
makes it narrower or wider.
[source,text/html]
----
<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
var rect = document.querySelector("div");
function updateWidth(add) {
var width = Math.max(10, rect.offsetWidth + add);
rect.style.width = width + "px";
}
var dragging = false, prevX;
rect.addEventListener("mousedown", function(event) {
dragging = true;
prevX = event.pageX;
event.preventDefault(); // Prevent selection
});
addEventListener("mouseup", function() {
dragging = false;
});
addEventListener("mousemove", function(event) {
if (dragging) {
updateWidth(event.pageX - prevX);
prevX = event.pageX;
}
});
</script>
----
Note that the `"mouseup"` and `"mousemove"` handlers are registered on the
whole window. Even if the mouse goes outside of the bar during
resizing, we still want to update its size and stop dragging when the
mouse is released.
Whenever the mouse pointer enters or leaves a node, the `"mouseover"`
or `"mouseout"` event is fired on this node. This can be used, among
other things, to create hover effects. But be careful, because these
events bubble just like other events, and thus you might
inconveniently receive a `"mouseout"` event for a node when the mouse
leaves one of its child nodes, which is not the point where you want
to turn off your hover effect.
To work around it, we can use the `relatedTarget` property of the
event objects created for these events. It tells us, in the case of
`"mouseover"`, what element the pointer was over before, and in the
case of `"mouseout"`, what element it is going to. We only want to
change our hover effect when the `relatedTarget` is outside of our
target node. When that is the case, it means that this event actually
represents a _crossing over_ from outside to inside the node (or the
other way around).
[source,text/html]
----
<p>Hover over this <strong>paragraph</strong>.</p>
<script>
var para = document.querySelector("p");
function isInside(node, target) {
for (; node != null; node = node.parentNode)
if (node == target) return true;
}
para.addEventListener("mouseover", function(event) {
if (!isInside(event.relatedTarget, para))
para.style.color = "red";
});
para.addEventListener("mouseout", function(event) {
if (!isInside(event.relatedTarget, para))
para.style.color = "";
});
</script>
----
The `isInside` function follows the given node's parent links until it
either reaches the top of the document (when `node` becomes null), or
finds the parent we are looking for.
I should add that the effect above can be much more easily achieved
using the CSS _pseudo-selector_ `:hover`, as shown below. But when you
need to react to the mouse entering or leaving a node in a way that
does not just change some style, the technique shown above is needed.
[source,text/html]
----
<style>
p:hover { color: red }
</style>
<p>Hover over this <strong>paragraph</strong>.</p>
----
== Scroll events ==
Whenever an element is scrolled, a `"scroll"` event is fired on it.
This has various uses, such as knowing what the user is currently
looking at (either to disable off-screen animation or to send secret
spy reports to your evil headquarters), or showing some indication of
progress (by highlighting a part of a document overview, or showing a
page or slide number).
The example below draws a progress bar in the top right corner of the
document, and updates it to fill up as you scroll down.
[source,text/html]
----
<style>
.progress {
border: 1px solid blue;
width: 100px;
position: fixed;
top: 10px; right: 10px;
}
.progress > div {
height: 12px;
background: blue;
width: 0%;
}
body {
height: 2000px;
}
</style>
<div class="progress"><div></div></div>
<p>Scroll me...</p>
<script>
var bar = document.querySelector(".progress div");
addEventListener("scroll", function() {
var max = document.body.scrollHeight - innerHeight;
var percent = (pageYOffset / max) * 100;
bar.style.width = percent + "%";
});
</script>
----
Giving an element a `position` of `fixed` acts much like an `absolute`
position, but also prevents it from scrolling along with the rest of
the document. This is used to make our progress bar stay in its
corner. Inside of it is another element, which is resized to indicate
the current progress. We use `%`, rather than `px` as a unit when
setting the width, so that the element is sized relative to the whole
bar.
The global `innerHeight` variable gives us the height of the window,
which we have to subtract from the total scrollable height, because
you can't scroll down anymore when the bottom of the screen has
reached the bottom of the document. (There is, of course, also
`innerWidth`.) By dividing `pageYOffset` (the current scroll position)
by the maximum scroll position, and multiplying that by a hundred, we
get the percentage that we want to display.
Calling `preventDefault` on a scroll event does not prevent the
scrolling from happening. In fact, the event handler is only called
_after_ the scrolling took place.
== Focus events ==
When an element is focused, the browser fires a `"focus"` event on it.
When it loses focus, a `"blur"` event fires.
Unlike to the events discussed earlier, these two events do not
_bubble_. A handler on a parent element is not notified when a child
element is focused or unfocused.
The example below displays a help text for the text field that is
currently focused.
[source,text/html]
----
<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Age in years"></p>
<p id="help"></p>
<script>
var help = document.querySelector("#help");
var fields = document.querySelectorAll("input");
for (var i = 0; i < fields.length; i++) {
fields[i].addEventListener("focus", function(event) {
var text = event.target.getAttribute("data-help");
help.textContent = text;
});
fields[i].addEventListener("blur", function(event) {
help.textContent = "";
});
}
</script>
----
The window object will receive `"focus"` and `"blur"` events when the
user moves from or to the tab or window in which the document is
shown.
== Load event ==
When page finishes loading, the `"load"` event is fired on the window
and the body. This is often used to schedule actions that initialize
something, but require the whole document to have been built up.
Remember that the content of `<script>` tags is run immediately, when
the tag is encountered. This is often too soon, for example when the
script needs to do something with parts of the document that appear
after the `<script>` tag.
Elements like images and script tags that load an external file also
have a `"load"` event that indicates the files they reference were
loaded. Like the focus-related events, loading events do not bubble.
When a page is closed or navigated away from (for example by following
a link), a `"beforeunload"` event is fired. The main use of this event
is to prevent the user from accidentally losing work by closing a
document. Doing this is not, as you might expect, done with the
`preventDefault` method. Instead, it is done by returning a string
from the handler. The string will be used in a dialog that asks the
user if they want to stay on the page or leave it. This mechanism
ensures that a user is able to leave the page, even if it running a
malicious script that would prefer to keep them there forever, in
order to force them to look at dodgy weight loss ads.
== Script execution timeline ==
There are various things that can cause a script to start executing.
Reading a `<script>` tag is obviously one such thing. An event firing,
if that even has a handler, is another. Chapter 13 discussed the
`requestAnimationFrame` function, which schedules a function to be
called before the next page redraw. That is yet another way in which a
script can start running.
It is important to understand that, even though events can fire at any
time, no two scripts in a single document ever run at the same moment.
If a script is already running, event handlers and pieces of program
scheduled in other ways have to wait for their turn. This is also the
reason why a document will freeze when a script runs for a long time.
The browser can not react to clicks and other events inside the
document, because it can not run event handlers until the current
script finishes running.
Some programming environments do allow multiple _threads of execution_
to run at the same time. Doing multiple things at the same time can be
used to make a program faster. But when you have multiple actors
touching the same parts of the system at the same time, thinking about
a program becomes at least an order of magnitude harder.
The fact that JavaScript programs only do one thing at a time thus
makes our life easier. For cases where you _really_ do want to do some
time-consuming thing in the background, without freezing the document,
browsers provide something called _Web Workers_. A worker is an
isolated JavaScript environment, which runs alongside the main program
for a document, and can only communicate with it by sending and
receiving messages.
Assume we have this code in `js/squareworker.js`, which is a program
that computes squares, which we will run inside of an isolated worker
process.
[source,javascript]
----
addEventListener("message", function(event) {
postMessage(event.data * event.data);
});
----
Imagine that squaring a number is a heavy, long-running computation
that we want to perform in a background thread. This code spawns a
worker, sends it a few messages, and outputs the responses.
// test: no
[source,javascript]
----
var squareWorker = new Worker("js/squareworker.js");
squareWorker.addEventListener("message", function(event) {
console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);
----
The `postMessage` function sends a message, which will cause a
`"message"` event to fire in the receiver. The script that created the
worker sends and receives messages through the `Worker` object,
whereas the worker talks to the script that created it by sending and
listening directly on its global scope—which is a _new_ global scope,
not shared with the original script.
== Setting timers ==
The `setTimeout` function is similar to `requestAnimationFrame`. It
schedules another function to be called later. But instead of having
it called at the next redraw, it is given an amount of milliseconds to
wait before calling the function. This page turns from blue to yellow
after two seconds:
[source,text/html]
----
<script>
document.body.style.background = "blue";
setTimeout(function() {
document.body.style.background = "yellow";
}, 2000);
</script>
----
Sometimes you need to cancel a function you have scheduled. This is
done by storing the value returned by `setTimeout`, and calling
`clearTimeout` on it.
[source,javascript]
----
var bombTimer = setTimeout(function() {
console.log("BOOM!");
}, 500);
if (Math.random() < .5) { // 50% chance
console.log("Defused.");
clearTimeout(bombTimer);
}
----
The `cancelAnimationFrame` function works in the same way as
++clearTimeout++—calling it on a value returned by
`requestAnimationFrame` will cancel that frame (assuming it hasn't
already been called).
A similar set of functions, `setInterval` and `clearInterval` are used
to set timers that should repeat every X milliseconds.
[source,javascript]
----
var ticks = 0;
var clock = setInterval(function() {
console.log("tick", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("stop.");
}
}, 200);
----
== Debouncing ==
Some types of events have the potential to fire rapidly, many times in
a row. The `"mousemove"` and `"scroll"` events, for example. When
handling such events, you must be careful not to do anything too
time-consuming, or your handler will take up so much time that
interaction with the document starts to feel slow and choppy.
If you do need to do something non-trivial in such a handler, you can
use `setTimeout` to make sure you are not doing it too often. This is
usually called _debouncing_ the event. There are several slightly
different approaches to this.
In this first example, we want to do something when the user has typed
something, but we don't want to do it immediately for every key event.
When they are typing quickly, we just want to wait until a pause
occurs. This is done by not immediately performing an action in the
event handler, but setting a timeout instead. We also clear the
previous timeout (if any), so that when events occur close together
(closer than our timeout delay), the timeout from the previous event
will be canceled.
[source,text/html]
----
<textarea>Type something here...</textarea>
<script>
var textarea = document.querySelector("textarea");
var timeout;
textarea.addEventListener("keydown", function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
console.log("You stopped typing.");
}, 500);
});
</script>
----
Giving an undefined value to `clearTimeout` or calling it on a timeout
that already fired has no effect. Thus, we don't have to be careful
about when to call it, and simply do so for every event.
A slightly different pattern occurs when we want to space responses to
an event at least a certain amount of time apart, but do want to fire
them _during_ a series of events, not just afterwards. For example, we
want to respond to `"mousemove"` events by showing the current
coordinates of the mouse, but only every 250 milliseconds.
[source,text/html]
----
<script>
var pending = false;
addEventListener("mousemove", function(event) {
if (!pending) {
pending = true;
setTimeout(function() {
pending = false;
document.body.textContent =
"Mouse at " + event.pageX + ", " + event.pageY;
}, 250);
}
});
</script>
----
== Summary ==
Event handlers make it possible to detect and react to events we have
no direct control over. The `addEventHandler` method is used to
register such a handler.
Each event has a name (`"keydown"`, `"focus"`, etc.) that identifies
it. Most events are called on a specific DOM elements, and then
_bubble_ up, allowing handlers associated with the parents of that
element to handle them.
When an event handler is called, it is passed an event object, with
additional information about the event. This object also has methods
that allow us to prevent further bubbling (`stopPropagation`) and the
browser's default handling of the event (`preventDefault`).
Pressing a key fires `"keydown"`, `"keypress"`, and `"keyup"` events.
Pressing a mouse button fires `"mousedown"`, `"mouseup"`, and
`"click"` events. Moving the mouse fires `"mousemove"` and possible
`"mouseenter"` and `"mouseout"` events.
Scrolling can be detected with the `"scroll"` event, and focus changes
with the `"focus"` and `"blur"` events. When the document finishes
loading, a `"load"` event fires on the window.
Only one piece of JavaScript program can run at a time. Thus, event
handlers and other scheduled scripts have to wait until other scripts
finish, before they get their turn.
== Excercises ==
=== Censored keyboard ===
Between 1928 and 2013, Turkish law forbade the use of the letters “Q”,
“W”, and “X” in official documents. This was part of a wider
initiative to stifle Kurdish culture—those letters occur in the
language used by Kurdish people, but not in regular Turkish.
As an exercise in doing ridiculous things with technology, I want to
ask you to program a text field (an `<input type="text">` tag) in such
a way that these letters can not be typed into it.
(Do not worry about copy-paste and other such loopholes.)
ifdef::html_target[]
// test: no
[source,text/html]
----
<input type="text">
<script>
var field = document.querySelector("input");
// Your code here.
</script>
----
endif::html_target[]
!!solution!!
The solution to this exercise involves preventing the default behavior
of key events. You can handle either `"keypress"` (the most obvious)
or `"keydown"`. If either of them has `preventDefault` called on it,
the letter will not appear.
Identifying the letter typed requires looking at the `keyCode` or
`charCode` property, and comparing that with the codes for the letters
we want to filter. In `"keydown"`, you do not have to worry about
lower- and upper-case letters, since it only identifies the key
pressed. If you decided to handle `"keypress"` instead, which
identifies actual characters typed, you have to make sure you test
both for both cases. One way to do that would be this:
----
/[qwx]/i.test(String.fromCharCode(event.charCode))
----
!!solution!!
=== Mouse trail ===
In JavaScript's early days, which was the high time of gaudy
homepages with lots of animated images, people came up with some truly
inspiring ways to use the language.
One of these was the “mouse trail”—a series of images following the
mouse as you moved it across the page.
In this exercise, I want you to implement a mouse trail. Use
absolutely positioned `<div>` elements with a fixed size and
background color (refer back to the link:#c_nplPzKchTA[code] in the
section on mouse click events for an example). Create twelve such
elements, and when the mouse moves, display them in the wake of the
mouse pointer, somehow.
There are various possible approaches here. You can make your solution
as complex as you want. To start with, a simple solution is to keep a
counter variable to cycle through your trail elements. Listen to
`"mousemove"` events, and every time one is fired, move the next
element to the point where the cursor currently is.
ifdef::html_target[]
// test: no
[source,text/html]
----
<style>
.trail { /* className for the trail elements */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>
<script>
// Your code here.
</script>
----
endif::html_target[]
!!solution!!
Creating the elements is best done in a loop. Append them to the
document to make them show up. In order to be able to access them
later, to change their position, store the trail elements in an array.
Cycling through them can be done by keeping a counter variable, and
adding one to it every time the `"mousemove"` event fires. The
remainder operator (`% 10`) can then be used to get a valid array
index, to pick the element we want to position during a given event.
Another interesting effect can be gotten by modeling a simple physics
system. The `"mousemove"` event is then used only to update a pair of
variables that track the mouse position. The animation is created by
using `requestAnimationFrame` to simulate the trailing elements being
attracted by the position of the mouse pointer. Every animation step,
their position is updated based on their relative position to the
pointer (and optionally, a speed that is stored for each element).
Figuring out a good way to do this is up to you.
!!solution!!
=== Tabs ===
A tabbed interface is a commonly used pattern. It allows you to select
from a number of views by selecting little clips “sticking out” above
an element.
In this exercise we implement a crude tab interface. Write a function
`asTabs` that, given a DOM node, creates a tabbed interface showing
the children of that node. It should insert a list of `<button>`
elements at the top of the node, one for each child node, containing
text retrieved from the `data-tabname` attributes of the child node.
All but one of the original children should be hidden (given a
`display` style of `none`), and the currently visible node can be
selected by clicking the buttons.
When it works, extend it to also style the currently active button
differently.
ifdef::html_target[]
// test: no
[source,text/html]
----
<div id="wrapper">
<div data-tabname="one">Tab one</div>
<div data-tabname="two">Tab two</div>
<div data-tabname="three">Tab three</div>
</div>
<script>
function asTabs(node) {
// Your code here.
}
asTabs(document.querySelector("#wrapper"));
</script>
----
endif::html_target[]
!!solution!!
One pitfall you'll probably run into is that you can't directly use
the node's `childNodes` property as a collection of tab nodes. For one
thing, when you add the buttons, they will also become a child node,
and end up in this pseudo-array. For another, the text nodes created
for the whitespace between the nodes are also in there, and should not
get their own tabs.