forked from marijnh/Eloquent-JavaScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
14_event.txt
980 lines (823 loc) · 33.4 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
: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 somewhat different approach to control than the
top-to-bottom model we have been working with.
== 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.
If you were doing some expensive computation, 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. The way browsers
do this is to allow 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
window, so the context is the whole window. Every DOM node has an
`addEventListener` method, which allows you to listen specifically on
that node.
[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, with the result
that only clicks on the button cause the 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, which makes it harder to accidentally
replace a handler left 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 again, 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. If we, for
example, want to know _which_ mouse button was pressed, we can look at
its `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.
Its `type` property 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. The handler for the button calls
`stopPropagation`, preventing the handler on the paragraph from
running, only when the click used the right mouse button.
[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.innerText);
});
</script>
----
== Default actions ==
Many events have a default action associated with them by the browser.
If you click a link, it will take you to the link's target. If you
press the down arrow, it will scroll the page down. A right click will
open a context menu. And so on.
For most types of events, the JavaScript event handlers are called
_before_ the default behavior is performed. If they performed an
action in response to the event, they can call the `preventDefault`
method on the event object in order to prevent the browser from
executing its default behavior.
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 the default
behavior they were expecting inexplicably fails to work.
Depending on the browser, some events can not be intercepted. On
Chrome, for example, keyboard shortcuts to close the current tab or
open a new window (Control-W and Control-N) can not be handled by
JavaScript.
== Key events ==
When a key on the keyboard is pressed down, your browser will fire a
`"keydown"` event. When it is released again, a `"keyup"` event is
fired. This enables various ways of responding to keys. You can react
to `"keydown"` to do something as soon as a key is hit, or react to
`"keyup"` to act once it has been “typed”—pressed an released again.
Finally, you can make keys have an effect as long as they are held, by
enabling the effect on `"keydown"`, and turning it off again on
`"keyup"`.
[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 acceleration
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 to detect key
combinations, whether they are held or not can be determined by
looking at the `shiftKey`, `ctrlKey`, `altKey`, and `metaKey`
properties of key (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. Sometimes you want to know which
character is being typed instead. For that purpose, the `"keypress"`
event is useful. 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 buttons and text fields
can. 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. One way this can
be useful is when you are implementing some form of mouse-dragging
functionality.
As an example, we display a square, and we want dragging over it to
cause its size to change—dragging down and right makes it bigger, up
and left makes it smaller.
[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 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. This is very inconvenient.
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; 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>
----
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 above technique 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 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.innerText = text;
});
fields[i].addEventListener("blur", function(event) {
help.innerText = "";
});
}
</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 things that require all
of the document to have been built up (remember that the content of
`<script>` tags is run immediately, when the tag is encountered).
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 lose 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 (showing
them 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.
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 doing
things 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 quite a bit easier. For when 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 the program
that will be run inside of the isolated worker process.
[source,javascript]
----
addEventListener("message", function(event) {
postMessage(event.data * event.data);
});
----
If we 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 want to cancel a scheduled function. 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 consume 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 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.innerText =
"Mouse at " + event.pageX + ", " + event.pageY;
}, 250);
}
});
</script>
----
== 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 typed 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. In `"keypress"`, 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
garish ways to use it.
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 in this 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 is also in there, and should not
get its own tab.
To work around this, start by building up a real array of all the
`nodeType` 1 children in the wrapper.
When registering event handlers on the buttons, you will probably need
to have your handler functions close over something that tells them
which tab node is associated with this button. A regular loop index
can be closed over, but all the handlers will be closing over the same
variable, which ends up with the value that it has at the end of the
loop.
A simple workaround is to use the `forEach` method and create the
handler function from inside the method's argument function, where it
can close over the function's arguments.
!!solution!!