From beead61f126fb39396f885b63edf2c661b3fb52c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 May 2020 00:39:33 +0300 Subject: [PATCH 1/7] renumber --- article.md | 208 +++++++++++++++++++++++++++++++++++++ ball-2.view/index.html | 38 +++++++ ball.view/index.html | 30 ++++++ multitouch.view/index.html | 28 +++++ slider.view/index.html | 37 +++++++ slider.view/style.css | 19 ++++ 6 files changed, 360 insertions(+) create mode 100644 article.md create mode 100644 ball-2.view/index.html create mode 100644 ball.view/index.html create mode 100644 multitouch.view/index.html create mode 100644 slider.view/index.html create mode 100644 slider.view/style.css diff --git a/article.md b/article.md new file mode 100644 index 000000000..e4c5a1cf9 --- /dev/null +++ b/article.md @@ -0,0 +1,208 @@ +# Pointer events + +Pointer events is a modern way to handle input from a variety of pointing devices, such as a mouse, a pen/stylus, a touchscreen and so on. + +## The brief history + +Let's make a small overview, so that you understand the general picture and the place of Pointer Events among other event types. + +- Long ago, in the past, there existed only mouse events. + + Then touch devices appeared. For the old code to work, they also generate mouse events. For instance, tapping generates `mousedown`. But mouse events were not good enough, as touch devices are more powerful in many aspects. For example, it's possible to touch multiple points at once, and mouse events don't have any properties for that. + +- So touch events were introduced, such as `touchstart`, `touchend`, `touchmove`, that have touch-specific properties (we don't cover them in details here, because pointer events are event better). + + Still, it wasn't enough, as there are many other devices, such as pens, that have their own features. Also, writing a code that listens both touch and mouse events was cumbersome. + +- To solve these issues, the new standard Pointer Events was introduced. It provides a single set of events for all kinds of pointing devices. + +As of now, [Pointer Events Level 2](https://www.w3.org/TR/pointerevents2/) specification is supported in all major browsers, while the [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works. Unless you code for Internet Explorer 10- and Safari 12-, there's no point in using mouse or touch events any more. We can switch to pointer events. + +That said, there are important peculiarities, one should know them to use them correctly and avoid extra surprises. We'll pay attention to them in this article. + +## Pointer event types + +Pointer events are named similar to mouse events: + +| Pointer Event | Mouse event | +|---------------|-------------| +| `pointerdown` | `mousedown` | +| `pointerup` | `mouseup` | +| `pointermove` | `mousemove` | +| `pointerover` | `mouseover` | +| `pointerout` | `mouseout` | +| `pointerenter` | `mouseenter` | +| `pointerleave` | `mouseleave` | +| `pointercancel` | - | +| `gotpointercapture` | - | +| `lostpointercapture` | - | + +As we can see, for every `mouse`, there's a `pointer` that plays a similar role. Also there are 3 additional pointer events that don't have a corresponding `mouse...` counterpart, we'll soon explain about them. + +## Pointer event properties + +Pointer events have the same properties as mouse events, such as `clientX/Y`, `target` etc, plus some extra: + +- `pointerId` - the unique identifier of the pointer causing the event. +- `pointerType` - the pointing device type, must be a string, one of: "mouse", "pen" or "touch". Can use this to react differently on these device types. +- `isPrimary` - `true` for the primary pointer, used to handle multi-touch, explained below. + +For pointers that have a changing contact area, e.g. a finger on the touchpad, these can be useful: + +- `width` - the width of of the area where the pointer touches the device. Where unsupported, e.g. for mouse it's always `1`. +- `height` - the height of of the area where the pointer touches the device. Where unsupported, always `1`. + +Other properties (rarely used): +- `pressure` - the pressure of the pointer tip, in range from 0 to 1. For devices that don't support pressure must be either `0.5` (pressed) or `0`. +- `tangentialPressure` - the normalized tangential pressure. +- `tiltX`, `tiltY`, `twist` - pen-specific properties that describe how the pen is positioned relative the surface. + +The last set of properties is supported by few special devices, you can find the details in the [specification](https://w3c.github.io/pointerevents/#pointerevent-interface) if ever needed. + +Let's see some examples where these properties may be helpful. + +## Multi-touch + +Phones and tablets, other devices that employ touchscreens, usually support multi-touch: a user can touch them in several places at once. + +Pointer Events allow to handle multi-touch with the help of `pointerId` and `isPrimary` properties. + +What happens when we touch a screen at one place, and then put another finger somewhere else? + +1. At the first touch we get `pointerdown` with `isPrimary=true` and some `pointerId`. +2. For the second finger and further touches we get `pointerdown` with `isPrimary=false` and a different `pointerId`. + +Please note: there's a `pointerId` for each pointer - a touching finger. If we use 5 fingers to simultaneously touch the screen, we have 5 `pointerdown` events with respective coordinates and different `pointerId`. + +The events associated with the first finger always have `isPrimary=true`. + +Whether we move and then detouch a finger, we get `pointermove` and `pointerup` events with the same `pointerId` as we had in `pointerdown`. So we can track multiple touching fingers using their `pointerId`. + +```online +Here's the demo that logs `pointerdown` and `pointerup` events: + +[iframe src="multitouch" edit height=200] + +Please note: you must be using a touchscreen device, such as a phone or a tablet to actually see the difference. For single-touch devices, such as a mouse, there'll be always same `pointerId` with `isPrimary=true`. +``` + +## Event: pointercancel + +This event fires when there's an ongoing pointer interaction, and then something happens that causes it to be aborted, so that no more pointer events are generated. + +Such causes are: +- The pointer device hardware was disabled. +- The device orientation changed (tablet rotated). +- The browser decided to handle the interaction on its own, considering it a mouse gesture or zoom-and-pan action or something else. + +The last reason is the most common and important one. So we'll demonstrate it on a practical example. + +Let's say we're impelementing drag'n'drop for a ball, just as in the beginning of the article . + +Here are the user actions and corresponding events: + +1) The user presses the mouse button on an image + - `pointerdown` event fires +2) Then they start dragging the image + - `pointermove` fires, maybe several times +3) The browser has native drag'n'drop support for images, that kicks in and takes over the drag'n'drop, thus generating `pointercancel` event. + - The browser now hangles drag'n'drop of the image. + - No more `pointermove` events. Our code doesn't work any more! + +The browser "hijacks" the interaction: `pointercancel` fires and no more `pointermove` events are generated! + +```online +Here's the demo with pointer events (only `up/down`, `move` and `cancel`) logged in the textarea: + +[iframe src="ball" height=240 edit] +``` + +**Prevent default browser actions to avoid `pointercancel`.** + +We need to do two things: + +1. Prevent drag'n'drop, e.g. by setting `ball.ondragstart = () => false`, just as described in the article . +2. For touch devices, there are also touch-related browser actions. We'll have problems with them too, so we should prevent them by setting `#ball { touch-action: none }` in CSS. That's required for our code to work on touch devices. + +Then the events work as intended, the browser doesn't hijack the process and no `pointercancel` triggers. + +```online +This demo adds these lines: + +[iframe src="ball-2" height=240 edit] + +As you can see, there's no `pointercancel` any more. +``` + +Now we can add the code to actually move the ball, and our drag'n'drop will work for mouse devices and touch devices. + +## Pointer capturing + +Pointer capturing is an interesting feature of pointer events. + +The idea is that we can "bind" all events with a particular `pointerId` to a given element. Then all subsequent events with the same `pointerId` will be retargeted to the same element. + +The related methods are: +- `elem.setPointerCapture(pointerId)` - binds the given `pointerId` to `elem`. +- `elem.releasePointerCapture(pointerId)` - unbinds the given `pointerId` from `elem`. + +Such binding doesn't hold long! It's automatically removed after `pointerup` or `pointercancel` events, or when the target `elem` is removed from the document. + +**Pointer capturing is used to simplify drag'n'drop kind of interactions.** + +We've already met the problem when making a custom slider in the article . + +1) First, the user should press `pointerdown` on the slider thumb to start dragging it. +2) ...But then the pointer may leave the slider and go elsewhere: below or over it, but `pointermove` events should still be tracked, and the thumb moved. + +Previously, to handle `pointermove` events that happen outside of the slider, we used `pointermove` events on the whole `document`. + +Pointer capturing provides an alternative solution: we can `thumb.setPointerCapture(event.pointerId)` in `pointerdown` handler, and then all future pointer events until `pointerup` will be retarteted to `thumb`. + +That is: events handlers on `thumb` will be called, and `event.target` will always be `thumb`, even if the user moves their pointer around the whole document. + +Here's the essential code: + +```js +thumb.onpointerdown = function(event) { + // retarget all pointer events (until pointerup) to me + thumb.setPointerCapture(event.pointerId); +}; + +thumb.onpointermove = function(event) { + // move the slider + let newLeft = event.clientX - slider.getBoundingClientRect().left; + + thumb.style.left = newLeft + 'px'; +}; +// no need to call thumb.releasePointerCapture, happens on pointerup automatically +``` + +```online +The full demo: + +[iframe src="slider" height=100 edit] +``` + +As a summary: the code becomes cleaner as we don't need to add/remove handlers on the whole `document` any more. + +There are two associated pointer events: + +- `gotpointercapture` fires when an element uses `setPointerCapture` to enable capturing. +- `lostpointercapture` fires when the capture is released: either explicitly with `releasePointerCapture` call, or automatically on `pointerup`/`pointercancel`. + +## Summary + +Pointer events allow to handle mouse, touch and pen events simultaneously. + +Pointer events extend mouse events. We can replace `mouse` with `pointer` in event names and expect our code to continue working for mouse, with better support for other device types. + +Remember to set `touch-events: none` in CSS for elements, otherwise the browser hijacks many types of touch interactions and pointer events won't be generated. + +Additional abilities of Pointer events are: + +- Multi-touch support using `pointerId` and `isPrimary`. +- Device-specific properties, such as `pressure`, `width/height` and others. +- Pointer capturing: we can retarget all pointer events to a specific element until `pointerup`/`pointercancel`. + +As of now, pointer events are supported in all major browsers, so we can safely switch to them, if IE10- and Safari 12- is not needed. And even with those browsers, there are polyfills that enable the support of pointer events. \ No newline at end of file diff --git a/ball-2.view/index.html b/ball-2.view/index.html new file mode 100644 index 000000000..5f3abbcb0 --- /dev/null +++ b/ball-2.view/index.html @@ -0,0 +1,38 @@ + + + + +

Drag the ball.

+ + + + + + + diff --git a/ball.view/index.html b/ball.view/index.html new file mode 100644 index 000000000..8bbef8f63 --- /dev/null +++ b/ball.view/index.html @@ -0,0 +1,30 @@ + + +

Drag the ball.

+ + + + + + + diff --git a/multitouch.view/index.html b/multitouch.view/index.html new file mode 100644 index 000000000..d46e1bc16 --- /dev/null +++ b/multitouch.view/index.html @@ -0,0 +1,28 @@ + + + + +
+ Multi-touch here +
+ + + diff --git a/slider.view/index.html b/slider.view/index.html new file mode 100644 index 000000000..2c2a69ec7 --- /dev/null +++ b/slider.view/index.html @@ -0,0 +1,37 @@ + + + +
+
+
+ + diff --git a/slider.view/style.css b/slider.view/style.css new file mode 100644 index 000000000..9b3d3b82d --- /dev/null +++ b/slider.view/style.css @@ -0,0 +1,19 @@ +.slider { + border-radius: 5px; + background: #E0E0E0; + background: linear-gradient(left top, #E0E0E0, #EEEEEE); + width: 310px; + height: 15px; + margin: 5px; +} + +.thumb { + width: 10px; + height: 25px; + border-radius: 3px; + position: relative; + left: 10px; + top: -5px; + background: blue; + cursor: pointer; +} From 0da70b6bb6a0fc0cab2a3a4f6695b8844b92cf1c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 May 2020 00:39:33 +0300 Subject: [PATCH 2/7] renumber --- 2-check-sync-keydown/solution.md | 6 + 2-check-sync-keydown/solution.view/index.html | 47 +++++ 2-check-sync-keydown/task.md | 19 ++ article.md | 189 ++++++++++++++++++ german-layout.svg | 1 + keyboard-dump.view/index.html | 38 ++++ keyboard-dump.view/script.js | 28 +++ keyboard-dump.view/style.css | 18 ++ us-layout.svg | 1 + 9 files changed, 347 insertions(+) create mode 100644 2-check-sync-keydown/solution.md create mode 100644 2-check-sync-keydown/solution.view/index.html create mode 100644 2-check-sync-keydown/task.md create mode 100644 article.md create mode 100644 german-layout.svg create mode 100644 keyboard-dump.view/index.html create mode 100644 keyboard-dump.view/script.js create mode 100644 keyboard-dump.view/style.css create mode 100644 us-layout.svg diff --git a/2-check-sync-keydown/solution.md b/2-check-sync-keydown/solution.md new file mode 100644 index 000000000..453f8c946 --- /dev/null +++ b/2-check-sync-keydown/solution.md @@ -0,0 +1,6 @@ + +We should use two handlers: `document.onkeydown` and `document.onkeyup`. + +Let's create a set `pressed = new Set()` to keep currently pressed keys. + +The first handler adds to it, while the second one removes from it. Every time on `keydown` we check if we have enough keys pressed, and run the function if it is so. diff --git a/2-check-sync-keydown/solution.view/index.html b/2-check-sync-keydown/solution.view/index.html new file mode 100644 index 000000000..6e86d2455 --- /dev/null +++ b/2-check-sync-keydown/solution.view/index.html @@ -0,0 +1,47 @@ + + + + +

Press "Q" and "W" together (can be in any language).

+ + + + + + diff --git a/2-check-sync-keydown/task.md b/2-check-sync-keydown/task.md new file mode 100644 index 000000000..51c2fa2bd --- /dev/null +++ b/2-check-sync-keydown/task.md @@ -0,0 +1,19 @@ +importance: 5 + +--- + +# Extended hotkeys + +Create a function `runOnKeys(func, code1, code2, ... code_n)` that runs `func` on simultaneous pressing of keys with codes `code1`, `code2`, ..., `code_n`. + +For instance, the code below shows `alert` when `"Q"` and `"W"` are pressed together (in any language, with or without CapsLock) + +```js no-beautify +runOnKeys( + () => alert("Hello!"), + "KeyQ", + "KeyW" +); +``` + +[demo src="solution"] diff --git a/article.md b/article.md new file mode 100644 index 000000000..617852ccf --- /dev/null +++ b/article.md @@ -0,0 +1,189 @@ +# Keyboard: keydown and keyup + +Before we get to keyboard, please note that on modern devices there are other ways to "input something". For instance, people use speech recognition (especially on mobile devices) or copy/paste with the mouse. + +So if we want to track any input into an `` field, then keyboard events are not enough. There's another event named `input` to track changes of an `` field, by any means. And it may be a better choice for such task. We'll cover it later in the chapter . + +Keyboard events should be used when we want to handle keyboard actions (virtual keyboard also counts). For instance, to react on arrow keys `key:Up` and `key:Down` or hotkeys (including combinations of keys). + + +## Teststand [#keyboard-test-stand] + +```offline +To better understand keyboard events, you can use the [teststand](sandbox:keyboard-dump). +``` + +```online +To better understand keyboard events, you can use the teststand below. + +Try different key combinations in the text field. + +[codetabs src="keyboard-dump" height=480] +``` + + +## Keydown and keyup + +The `keydown` events happens when a key is pressed down, and then `keyup` -- when it's released. + +### event.code and event.key + +The `key` property of the event object allows to get the character, while the `code` property of the event object allows to get the "physical key code". + +For instance, the same key `key:Z` can be pressed with or without `key:Shift`. That gives us two different characters: lowercase `z` and uppercase `Z`. + +The `event.key` is exactly the character, and it will be different. But `event.code` is the same: + +| Key | `event.key` | `event.code` | +|--------------|-------------|--------------| +| `key:Z` |`z` (lowercase) |`KeyZ` | +| `key:Shift+Z`|`Z` (uppercase) |`KeyZ` | + + +If a user works with different languages, then switching to another language would make a totally different character instead of `"Z"`. That will become the value of `event.key`, while `event.code` is always the same: `"KeyZ"`. + +```smart header="\"KeyZ\" and other key codes" +Every key has the code that depends on its location on the keyboard. Key codes described in the [UI Events code specification](https://www.w3.org/TR/uievents-code/). + +For instance: +- Letter keys have codes `"Key"`: `"KeyA"`, `"KeyB"` etc. +- Digit keys have codes: `"Digit"`: `"Digit0"`, `"Digit1"` etc. +- Special keys are coded by their names: `"Enter"`, `"Backspace"`, `"Tab"` etc. + +There are several widespread keyboard layouts, and the specification gives key codes for each of them. + +Read the [alphanumeric section of the spec](https://www.w3.org/TR/uievents-code/#key-alphanumeric-section) for more codes, or just press a key in the [teststand](#keyboard-test-stand) above. +``` + +```warn header="Case matters: `\"KeyZ\"`, not `\"keyZ\"`" +Seems obvious, but people still make mistakes. + +Please evade mistypes: it's `KeyZ`, not `keyZ`. The check like `event.code=="keyZ"` won't work: the first letter of `"Key"` must be uppercase. +``` + +What if a key does not give any character? For instance, `key:Shift` or `key:F1` or others. For those keys, `event.key` is approximately the same as `event.code`: + +| Key | `event.key` | `event.code` | +|--------------|-------------|--------------| +| `key:F1` |`F1` |`F1` | +| `key:Backspace` |`Backspace` |`Backspace` | +| `key:Shift`|`Shift` |`ShiftRight` or `ShiftLeft` | + +Please note that `event.code` specifies exactly which key is pressed. For instance, most keyboards have two `key:Shift` keys: on the left and on the right side. The `event.code` tells us exactly which one was pressed, and `event.key` is responsible for the "meaning" of the key: what it is (a "Shift"). + +Let's say, we want to handle a hotkey: `key:Ctrl+Z` (or `key:Cmd+Z` for Mac). Most text editors hook the "Undo" action on it. We can set a listener on `keydown` and check which key is pressed. + +There's a dilemma here: in such a listener, should we check the value of `event.key` or `event.code`? + +On one hand, the value of `event.key` is a character, it changes depending on the language. If the visitor has several languages in OS and switches between them, the same key gives different characters. So it makes sense to check `event.code`, it's always the same. + +Like this: + +```js run +document.addEventListener('keydown', function(event) { + if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) { + alert('Undo!') + } +}); +``` + +On the other hand, there's a problem with `event.code`. For different keyboard layouts, the same key may have different characters. + +For example, here are US layout ("QWERTY") and German layout ("QWERTZ") under it (from Wikipedia): + +![](us-layout.svg) + +![](german-layout.svg) + +For the same key, US layout has "Z", while German layout has "Y" (letters are swapped). + +Literally, `event.code` will equal `KeyZ` for people with German layout when they press `key:Y`. + +If we check `event.code == 'KeyZ'` in our code, then for people with German layout such test will pass when they press `key:Y`. + +That sounds really odd, but so it is. The [specification](https://www.w3.org/TR/uievents-code/#table-key-code-alphanumeric-writing-system) explicitly mentions such behavior. + +So, `event.code` may match a wrong character for unexpected layout. Same letters in different layouts may map to different physical keys, leading to different codes. Luckily, that happens only with several codes, e.g. `keyA`, `keyQ`, `keyZ` (as we've seen), and doesn't happen with special keys such as `Shift`. You can find the list in the [specification](https://www.w3.org/TR/uievents-code/#table-key-code-alphanumeric-writing-system). + +To reliably track layout-dependent characters, `event.key` may be a better way. + +On the other hand, `event.code` has the benefit of staying always the same, bound to the physical key location, even if the visitor changes languages. So hotkeys that rely on it work well even in case of a language switch. + +Do we want to handle layout-dependant keys? Then `event.key` is the way to go. + +Or we want a hotkey to work even after a language switch? Then `event.code` may be better. + +## Auto-repeat + +If a key is being pressed for a long enough time, it starts to "auto-repeat": the `keydown` triggers again and again, and then when it's released we finally get `keyup`. So it's kind of normal to have many `keydown` and a single `keyup`. + +For events triggered by auto-repeat, the event object has `event.repeat` property set to `true`. + + +## Default actions + +Default actions vary, as there are many possible things that may be initiated by the keyboard. + +For instance: + +- A character appears on the screen (the most obvious outcome). +- A character is deleted (`key:Delete` key). +- The page is scrolled (`key:PageDown` key). +- The browser opens the "Save Page" dialog (`key:Ctrl+S`) +- ...and so on. + +Preventing the default action on `keydown` can cancel most of them, with the exception of OS-based special keys. For instance, on Windows `key:Alt+F4` closes the current browser window. And there's no way to stop it by preventing the default action in JavaScript. + +For instance, the `` below expects a phone number, so it does not accept keys except digits, `+`, `()` or `-`: + +```html autorun height=60 run + + +``` + +Please note that special keys, such as `key:Backspace`, `key:Left`, `key:Right`, `key:Ctrl+V`, do not work in the input. That's a side-effect of the strict filter `checkPhoneKey`. + +Let's relax it a little bit: + + +```html autorun height=60 run + + +``` + +Now arrows and deletion works well. + +...But we still can enter anything by using a mouse and right-click + Paste. So the filter is not 100% reliable. We can just let it be like that, because most of time it works. Or an alternative approach would be to track the `input` event -- it triggers after any modification. There we can check the new value and highlight/modify it when it's invalid. + +## Legacy + +In the past, there was a `keypress` event, and also `keyCode`, `charCode`, `which` properties of the event object. + +There were so many browser incompatibilities while working with them, that developers of the specification had no way, other than deprecating all of them and creating new, modern events (described above in this chapter). The old code still works, as browsers keep supporting them, but there's totally no need to use those any more. + +## Summary + +Pressing a key always generates a keyboard event, be it symbol keys or special keys like `key:Shift` or `key:Ctrl` and so on. The only exception is `key:Fn` key that sometimes presents on a laptop keyboard. There's no keyboard event for it, because it's often implemented on lower level than OS. + +Keyboard events: + +- `keydown` -- on pressing the key (auto-repeats if the key is pressed for long), +- `keyup` -- on releasing the key. + +Main keyboard event properties: + +- `code` -- the "key code" (`"KeyA"`, `"ArrowLeft"` and so on), specific to the physical location of the key on keyboard. +- `key` -- the character (`"A"`, `"a"` and so on), for non-character keys, such as `key:Esc`, usually has the same value as `code`. + +In the past, keyboard events were sometimes used to track user input in form fields. That's not reliable, because the input can come from various sources. We have `input` and `change` events to handle any input (covered later in the chapter ). They trigger after any kind of input, including copy-pasting or speech recognition. + +We should use keyboard events when we really want keyboard. For example, to react on hotkeys or special keys. diff --git a/german-layout.svg b/german-layout.svg new file mode 100644 index 000000000..8a880e8e0 --- /dev/null +++ b/german-layout.svg @@ -0,0 +1 @@ +StrgStrgAl tAlt GrWinWinMenu \ No newline at end of file diff --git a/keyboard-dump.view/index.html b/keyboard-dump.view/index.html new file mode 100644 index 000000000..401062830 --- /dev/null +++ b/keyboard-dump.view/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + +
+ + Prevent default for: +     + + +

+ Ignore: +     + +

+ +

Focus on the input field and press a key.

+ + + + + +
+ + + + + diff --git a/keyboard-dump.view/script.js b/keyboard-dump.view/script.js new file mode 100644 index 000000000..5eba24c7a --- /dev/null +++ b/keyboard-dump.view/script.js @@ -0,0 +1,28 @@ +kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle; + +let lastTime = Date.now(); + +function handle(e) { + if (form.elements[e.type + 'Ignore'].checked) return; + + let text = e.type + + ' key=' + e.key + + ' code=' + e.code + + (e.shiftKey ? ' shiftKey' : '') + + (e.ctrlKey ? ' ctrlKey' : '') + + (e.altKey ? ' altKey' : '') + + (e.metaKey ? ' metaKey' : '') + + (e.repeat ? ' (repeat)' : '') + + "\n"; + + if (area.value && Date.now() - lastTime > 250) { + area.value += new Array(81).join('-') + '\n'; + } + lastTime = Date.now(); + + area.value += text; + + if (form.elements[e.type + 'Stop'].checked) { + e.preventDefault(); + } +} diff --git a/keyboard-dump.view/style.css b/keyboard-dump.view/style.css new file mode 100644 index 000000000..8474832b1 --- /dev/null +++ b/keyboard-dump.view/style.css @@ -0,0 +1,18 @@ +#kinput { + font-size: 150%; + box-sizing: border-box; + width: 95%; +} + +#area { + width: 95%; + box-sizing: border-box; + height: 250px; + border: 1px solid black; + display: block; +} + +form label { + display: inline; + white-space: nowrap; +} \ No newline at end of file diff --git a/us-layout.svg b/us-layout.svg new file mode 100644 index 000000000..699277e02 --- /dev/null +++ b/us-layout.svg @@ -0,0 +1 @@ +Caps LockShiftShift \ No newline at end of file From 6b10e6b7c7c249462c3d5451a634edfab34bdbb5 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 10 May 2020 00:54:37 +0300 Subject: [PATCH 3/7] minor fixes --- article.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/article.md b/article.md index e4c5a1cf9..21d793380 100644 --- a/article.md +++ b/article.md @@ -205,4 +205,4 @@ Additional abilities of Pointer events are: - Device-specific properties, such as `pressure`, `width/height` and others. - Pointer capturing: we can retarget all pointer events to a specific element until `pointerup`/`pointercancel`. -As of now, pointer events are supported in all major browsers, so we can safely switch to them, if IE10- and Safari 12- is not needed. And even with those browsers, there are polyfills that enable the support of pointer events. \ No newline at end of file +As of now, pointer events are supported in all major browsers, so we can safely switch to them, if IE10- and Safari 12- are not needed. And even with those browsers, there are polyfills that enable the support of pointer events. \ No newline at end of file From a6c8c0593be6ccba230bfc622750d2da11869820 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 11 May 2020 10:31:59 +0300 Subject: [PATCH 4/7] minor fixes --- article.md | 103 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/article.md b/article.md index 21d793380..9c68f1cee 100644 --- a/article.md +++ b/article.md @@ -16,7 +16,7 @@ Let's make a small overview, so that you understand the general picture and the - To solve these issues, the new standard Pointer Events was introduced. It provides a single set of events for all kinds of pointing devices. -As of now, [Pointer Events Level 2](https://www.w3.org/TR/pointerevents2/) specification is supported in all major browsers, while the [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works. Unless you code for Internet Explorer 10- and Safari 12-, there's no point in using mouse or touch events any more. We can switch to pointer events. +As of now, [Pointer Events Level 2](https://www.w3.org/TR/pointerevents2/) specification is supported in all major browsers, while the [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works. Unless you code for Internet Explorer 10 or Safari 12 and below, there's no point in using mouse or touch events any more. We can switch to pointer events. That said, there are important peculiarities, one should know them to use them correctly and avoid extra surprises. We'll pay attention to them in this article. @@ -39,77 +39,87 @@ Pointer events are named similar to mouse events: As we can see, for every `mouse`, there's a `pointer` that plays a similar role. Also there are 3 additional pointer events that don't have a corresponding `mouse...` counterpart, we'll soon explain about them. +```smart header="Replacing `mouse` with `pointer` in our code" +We can replace `mouse` events with `pointer` in our code and expect things to continue working fine with mouse. + +The support for touch devices will also "magically" improve, but we'll probably need to add `touch-action: none` rule in CSS. See the details below in the section about `pointercancel`. +``` + ## Pointer event properties Pointer events have the same properties as mouse events, such as `clientX/Y`, `target` etc, plus some extra: - `pointerId` - the unique identifier of the pointer causing the event. -- `pointerType` - the pointing device type, must be a string, one of: "mouse", "pen" or "touch". Can use this to react differently on these device types. -- `isPrimary` - `true` for the primary pointer, used to handle multi-touch, explained below. + + Allows to handle multiple pointers, such as a touchscreen with stylus and multi-touch (explained below). +- `pointerType` - the pointing device type, must be a string, one of: "mouse", "pen" or "touch". -For pointers that have a changing contact area, e.g. a finger on the touchpad, these can be useful: + We can use this property to react differently on various pointer types. +- `isPrimary` - `true` for the primary pointer (the first finger in multi-touch). + +For pointers that measure a contact area and pressure, e.g. a finger on the touchscreen, the additional properties can be useful: - `width` - the width of of the area where the pointer touches the device. Where unsupported, e.g. for mouse it's always `1`. - `height` - the height of of the area where the pointer touches the device. Where unsupported, always `1`. - -Other properties (rarely used): - `pressure` - the pressure of the pointer tip, in range from 0 to 1. For devices that don't support pressure must be either `0.5` (pressed) or `0`. - `tangentialPressure` - the normalized tangential pressure. - `tiltX`, `tiltY`, `twist` - pen-specific properties that describe how the pen is positioned relative the surface. -The last set of properties is supported by few special devices, you can find the details in the [specification](https://w3c.github.io/pointerevents/#pointerevent-interface) if ever needed. - -Let's see some examples where these properties may be helpful. +These properties aren't very well supported across devices, so they are rarely used. You can find the details in the [specification](https://w3c.github.io/pointerevents/#pointerevent-interface) if needed. ## Multi-touch -Phones and tablets, other devices that employ touchscreens, usually support multi-touch: a user can touch them in several places at once. +One of the things that mouse events totally don't support is multi-touch: a user can touch them in several places at once at their phone or tablet, perform special gestures. Pointer Events allow to handle multi-touch with the help of `pointerId` and `isPrimary` properties. -What happens when we touch a screen at one place, and then put another finger somewhere else? +Here's what happens when a user touches a screen at one place, and then puts another finger somewhere else on it: -1. At the first touch we get `pointerdown` with `isPrimary=true` and some `pointerId`. -2. For the second finger and further touches we get `pointerdown` with `isPrimary=false` and a different `pointerId`. +1. At the first touch: + - `pointerdown` with `isPrimary=true` and some `pointerId`. +2. For the second finger and further touches: + - `pointerdown` with `isPrimary=false` and a different `pointerId` for every finger. -Please note: there's a `pointerId` for each pointer - a touching finger. If we use 5 fingers to simultaneously touch the screen, we have 5 `pointerdown` events with respective coordinates and different `pointerId`. +Please note: there `pointerId` is assigned not to the whole device, but for each touching finger. If we use 5 fingers to simultaneously touch the screen, we have 5 `pointerdown` events with respective coordinates and different `pointerId`. The events associated with the first finger always have `isPrimary=true`. -Whether we move and then detouch a finger, we get `pointermove` and `pointerup` events with the same `pointerId` as we had in `pointerdown`. So we can track multiple touching fingers using their `pointerId`. +We can track multiple touching fingers using their `pointerId`. When the user moves move and then detouches a finger, we get `pointermove` and `pointerup` events with the same `pointerId` as we had in `pointerdown`. ```online Here's the demo that logs `pointerdown` and `pointerup` events: [iframe src="multitouch" edit height=200] -Please note: you must be using a touchscreen device, such as a phone or a tablet to actually see the difference. For single-touch devices, such as a mouse, there'll be always same `pointerId` with `isPrimary=true`. +Please note: you must be using a touchscreen device, such as a phone or a tablet to actually see the difference. For single-touch devices, such as a mouse, there'll be always same `pointerId` with `isPrimary=true`, for all pointer events. ``` ## Event: pointercancel -This event fires when there's an ongoing pointer interaction, and then something happens that causes it to be aborted, so that no more pointer events are generated. +We've mentioned the importance of `touch-action: none` before. Now let's explain why, as skipping this may cause our interfaces to malfunction. + +The `pointercancel` event fires when there's an ongoing pointer interaction, and then something happens that causes it to be aborted, so that no more pointer events are generated. Such causes are: - The pointer device hardware was disabled. - The device orientation changed (tablet rotated). - The browser decided to handle the interaction on its own, considering it a mouse gesture or zoom-and-pan action or something else. -The last reason is the most common and important one. So we'll demonstrate it on a practical example. +We'll demonstrate `pointercancel` on a practical example to see how it affects us. Let's say we're impelementing drag'n'drop for a ball, just as in the beginning of the article . -Here are the user actions and corresponding events: +Here are the flow of user actions and corresponding events: -1) The user presses the mouse button on an image +1) The user presses the mouse button on an image, to start dragging - `pointerdown` event fires 2) Then they start dragging the image - `pointermove` fires, maybe several times -3) The browser has native drag'n'drop support for images, that kicks in and takes over the drag'n'drop, thus generating `pointercancel` event. - - The browser now hangles drag'n'drop of the image. - - No more `pointermove` events. Our code doesn't work any more! +3) Surprise! The browser has native drag'n'drop support for images, that kicks in and takes over the drag'n'drop process, thus generating `pointercancel` event. + - The browser now handles drag'n'drop of the image on its own. The user may even drag the ball image out of the browser, into their Mail program or a File Manager. + - No more `pointermove` events for us. -The browser "hijacks" the interaction: `pointercancel` fires and no more `pointermove` events are generated! +So the issue is that the browser "hijacks" the interaction: `pointercancel` fires and no more `pointermove` events are generated. ```online Here's the demo with pointer events (only `up/down`, `move` and `cancel`) logged in the textarea: @@ -117,14 +127,20 @@ Here's the demo with pointer events (only `up/down`, `move` and `cancel`) logged [iframe src="ball" height=240 edit] ``` +We'd like to implement our own drag'n'drop, so let's tell the browser not to take it over. + **Prevent default browser actions to avoid `pointercancel`.** We need to do two things: -1. Prevent drag'n'drop, e.g. by setting `ball.ondragstart = () => false`, just as described in the article . -2. For touch devices, there are also touch-related browser actions. We'll have problems with them too, so we should prevent them by setting `#ball { touch-action: none }` in CSS. That's required for our code to work on touch devices. +1. Prevent native drag'n'drop from happening: + - Can do it by setting `ball.ondragstart = () => false`, just as described in the article . + - That works well for mouse events. +2. For touch devices, there are also touch-related browser actions. We'll have problems with them too. + - We can prevent them by setting `#ball { touch-action: none }` in CSS. + - Then our code will start working on touch devices. -Then the events work as intended, the browser doesn't hijack the process and no `pointercancel` triggers. +After we do that, the events will work as intended, the browser won't hijack the process and emit no `pointercancel`. ```online This demo adds these lines: @@ -138,28 +154,32 @@ Now we can add the code to actually move the ball, and our drag'n'drop will work ## Pointer capturing -Pointer capturing is an interesting feature of pointer events. +Pointer capturing is a special feature of pointer events. -The idea is that we can "bind" all events with a particular `pointerId` to a given element. Then all subsequent events with the same `pointerId` will be retargeted to the same element. +The idea is that we can "bind" all events with a particular `pointerId` to a given element. Then all subsequent events with the same `pointerId` will be retargeted to the same element. That is: the browser sets that element as the target and trigger associated handlers, no matter where it actually happened. The related methods are: - `elem.setPointerCapture(pointerId)` - binds the given `pointerId` to `elem`. - `elem.releasePointerCapture(pointerId)` - unbinds the given `pointerId` from `elem`. -Such binding doesn't hold long! It's automatically removed after `pointerup` or `pointercancel` events, or when the target `elem` is removed from the document. +Such binding doesn't hold long. It's automatically removed after `pointerup` or `pointercancel` events, or when the target `elem` is removed from the document. + +Now when do we need this? **Pointer capturing is used to simplify drag'n'drop kind of interactions.** -We've already met the problem when making a custom slider in the article . +Let's recall the problem we met while making a custom slider in the article . -1) First, the user should press `pointerdown` on the slider thumb to start dragging it. -2) ...But then the pointer may leave the slider and go elsewhere: below or over it, but `pointermove` events should still be tracked, and the thumb moved. +1) First, the user presses `pointerdown` on the slider thumb to start dragging it. +2) ...But then, as they move the pointer, it may leave the slider: go below or over it. -Previously, to handle `pointermove` events that happen outside of the slider, we used `pointermove` events on the whole `document`. +But we continue tracking track `pointermove` events and move the thumb until `pointerup`, even though the pointer is not on the slider any more. -Pointer capturing provides an alternative solution: we can `thumb.setPointerCapture(event.pointerId)` in `pointerdown` handler, and then all future pointer events until `pointerup` will be retarteted to `thumb`. +[Previously](info:mouse-drag-and-drop), to handle `pointermove` events that happen outside of the slider, we listened for `pointermove` events on the whole `document`. -That is: events handlers on `thumb` will be called, and `event.target` will always be `thumb`, even if the user moves their pointer around the whole document. +Pointer capturing provides an alternative solution: we can call `thumb.setPointerCapture(event.pointerId)` in `pointerdown` handler, and then all future pointer events until `pointerup` will be retarteted to `thumb`. + +That is: events handlers on `thumb` will be called, and `event.target` will always be `thumb`, even if the user moves their pointer around the whole document. So we can listen at `thumb` for `pointermove`, no matter where it happens. Here's the essential code: @@ -170,12 +190,13 @@ thumb.onpointerdown = function(event) { }; thumb.onpointermove = function(event) { - // move the slider + // move the slider: listen at thumb, as all events are retargeted to it let newLeft = event.clientX - slider.getBoundingClientRect().left; - thumb.style.left = newLeft + 'px'; }; -// no need to call thumb.releasePointerCapture, happens on pointerup automatically + +// note: no need to call thumb.releasePointerCapture, +// it happens on pointerup automatically ``` ```online @@ -184,7 +205,7 @@ The full demo: [iframe src="slider" height=100 edit] ``` -As a summary: the code becomes cleaner as we don't need to add/remove handlers on the whole `document` any more. +**As a summary: the code becomes cleaner as we don't need to add/remove handlers on the whole `document` any more. That's what pointer capturing does.** There are two associated pointer events: @@ -197,7 +218,7 @@ Pointer events allow to handle mouse, touch and pen events simultaneously. Pointer events extend mouse events. We can replace `mouse` with `pointer` in event names and expect our code to continue working for mouse, with better support for other device types. -Remember to set `touch-events: none` in CSS for elements, otherwise the browser hijacks many types of touch interactions and pointer events won't be generated. +Remember to set `touch-events: none` in CSS for elements that we engage, otherwise the browser hijacks many types of touch interactions and pointer events won't be generated. Additional abilities of Pointer events are: From 0926569b696cd3f1fa2d6cada9dcab4fe1c4b881 Mon Sep 17 00:00:00 2001 From: odsantos Date: Wed, 20 May 2020 20:47:36 +0100 Subject: [PATCH 5/7] Update folder 3-event-details - add 6-pointer-events folder --- article.md => 2-ui/3-event-details/6-pointer-events/article.md | 0 .../3-event-details/6-pointer-events/ball-2.view}/index.html | 0 .../3-event-details/6-pointer-events/ball.view}/index.html | 0 .../3-event-details/6-pointer-events/multitouch.view}/index.html | 0 .../3-event-details/6-pointer-events/slider.view}/index.html | 0 .../3-event-details/6-pointer-events/slider.view}/style.css | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename article.md => 2-ui/3-event-details/6-pointer-events/article.md (100%) rename {ball-2.view => 2-ui/3-event-details/6-pointer-events/ball-2.view}/index.html (100%) rename {ball.view => 2-ui/3-event-details/6-pointer-events/ball.view}/index.html (100%) rename {multitouch.view => 2-ui/3-event-details/6-pointer-events/multitouch.view}/index.html (100%) rename {slider.view => 2-ui/3-event-details/6-pointer-events/slider.view}/index.html (100%) rename {slider.view => 2-ui/3-event-details/6-pointer-events/slider.view}/style.css (100%) diff --git a/article.md b/2-ui/3-event-details/6-pointer-events/article.md similarity index 100% rename from article.md rename to 2-ui/3-event-details/6-pointer-events/article.md diff --git a/ball-2.view/index.html b/2-ui/3-event-details/6-pointer-events/ball-2.view/index.html similarity index 100% rename from ball-2.view/index.html rename to 2-ui/3-event-details/6-pointer-events/ball-2.view/index.html diff --git a/ball.view/index.html b/2-ui/3-event-details/6-pointer-events/ball.view/index.html similarity index 100% rename from ball.view/index.html rename to 2-ui/3-event-details/6-pointer-events/ball.view/index.html diff --git a/multitouch.view/index.html b/2-ui/3-event-details/6-pointer-events/multitouch.view/index.html similarity index 100% rename from multitouch.view/index.html rename to 2-ui/3-event-details/6-pointer-events/multitouch.view/index.html diff --git a/slider.view/index.html b/2-ui/3-event-details/6-pointer-events/slider.view/index.html similarity index 100% rename from slider.view/index.html rename to 2-ui/3-event-details/6-pointer-events/slider.view/index.html diff --git a/slider.view/style.css b/2-ui/3-event-details/6-pointer-events/slider.view/style.css similarity index 100% rename from slider.view/style.css rename to 2-ui/3-event-details/6-pointer-events/slider.view/style.css From b12cd680dba71645e818099bd9aa41e9ff3e6a4b Mon Sep 17 00:00:00 2001 From: odsantos Date: Wed, 20 May 2020 21:03:47 +0100 Subject: [PATCH 6/7] Update folder 3-event-details - add 7-keyboard-events folder --- .../7-keyboard-events/2-check-sync-keydown}/solution.md | 0 .../2-check-sync-keydown}/solution.view/index.html | 0 .../7-keyboard-events/2-check-sync-keydown}/task.md | 0 article.md => 2-ui/3-event-details/7-keyboard-events/article.md | 0 .../3-event-details/7-keyboard-events/german-layout.svg | 0 .../7-keyboard-events/keyboard-dump.view}/index.html | 0 .../7-keyboard-events/keyboard-dump.view}/script.js | 0 .../7-keyboard-events/keyboard-dump.view}/style.css | 0 .../3-event-details/7-keyboard-events/us-layout.svg | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {2-check-sync-keydown => 2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown}/solution.md (100%) rename {2-check-sync-keydown => 2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown}/solution.view/index.html (100%) rename {2-check-sync-keydown => 2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown}/task.md (100%) rename article.md => 2-ui/3-event-details/7-keyboard-events/article.md (100%) rename german-layout.svg => 2-ui/3-event-details/7-keyboard-events/german-layout.svg (100%) rename {keyboard-dump.view => 2-ui/3-event-details/7-keyboard-events/keyboard-dump.view}/index.html (100%) rename {keyboard-dump.view => 2-ui/3-event-details/7-keyboard-events/keyboard-dump.view}/script.js (100%) rename {keyboard-dump.view => 2-ui/3-event-details/7-keyboard-events/keyboard-dump.view}/style.css (100%) rename us-layout.svg => 2-ui/3-event-details/7-keyboard-events/us-layout.svg (100%) diff --git a/2-check-sync-keydown/solution.md b/2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown/solution.md similarity index 100% rename from 2-check-sync-keydown/solution.md rename to 2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown/solution.md diff --git a/2-check-sync-keydown/solution.view/index.html b/2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown/solution.view/index.html similarity index 100% rename from 2-check-sync-keydown/solution.view/index.html rename to 2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown/solution.view/index.html diff --git a/2-check-sync-keydown/task.md b/2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown/task.md similarity index 100% rename from 2-check-sync-keydown/task.md rename to 2-ui/3-event-details/7-keyboard-events/2-check-sync-keydown/task.md diff --git a/article.md b/2-ui/3-event-details/7-keyboard-events/article.md similarity index 100% rename from article.md rename to 2-ui/3-event-details/7-keyboard-events/article.md diff --git a/german-layout.svg b/2-ui/3-event-details/7-keyboard-events/german-layout.svg similarity index 100% rename from german-layout.svg rename to 2-ui/3-event-details/7-keyboard-events/german-layout.svg diff --git a/keyboard-dump.view/index.html b/2-ui/3-event-details/7-keyboard-events/keyboard-dump.view/index.html similarity index 100% rename from keyboard-dump.view/index.html rename to 2-ui/3-event-details/7-keyboard-events/keyboard-dump.view/index.html diff --git a/keyboard-dump.view/script.js b/2-ui/3-event-details/7-keyboard-events/keyboard-dump.view/script.js similarity index 100% rename from keyboard-dump.view/script.js rename to 2-ui/3-event-details/7-keyboard-events/keyboard-dump.view/script.js diff --git a/keyboard-dump.view/style.css b/2-ui/3-event-details/7-keyboard-events/keyboard-dump.view/style.css similarity index 100% rename from keyboard-dump.view/style.css rename to 2-ui/3-event-details/7-keyboard-events/keyboard-dump.view/style.css diff --git a/us-layout.svg b/2-ui/3-event-details/7-keyboard-events/us-layout.svg similarity index 100% rename from us-layout.svg rename to 2-ui/3-event-details/7-keyboard-events/us-layout.svg From 4bcccd136a8493dd78076452a7c441079264f3a3 Mon Sep 17 00:00:00 2001 From: odsantos Date: Wed, 20 May 2020 21:08:30 +0100 Subject: [PATCH 7/7] Update folder 3-event-details --- .../2-check-sync-keydown/solution.md | 6 - .../solution.view/index.html | 47 ---- .../2-check-sync-keydown/task.md | 19 -- .../5-keyboard-events/article.md | 204 ------------------ .../keyboard-dump.view/index.html | 38 ---- .../keyboard-dump.view/script.js | 28 --- .../keyboard-dump.view/style.css | 18 -- 7 files changed, 360 deletions(-) delete mode 100644 2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.md delete mode 100644 2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.view/index.html delete mode 100644 2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/task.md delete mode 100644 2-ui/3-event-details/5-keyboard-events/article.md delete mode 100644 2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/index.html delete mode 100644 2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/script.js delete mode 100644 2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/style.css diff --git a/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.md b/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.md deleted file mode 100644 index 453f8c946..000000000 --- a/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.md +++ /dev/null @@ -1,6 +0,0 @@ - -We should use two handlers: `document.onkeydown` and `document.onkeyup`. - -Let's create a set `pressed = new Set()` to keep currently pressed keys. - -The first handler adds to it, while the second one removes from it. Every time on `keydown` we check if we have enough keys pressed, and run the function if it is so. diff --git a/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.view/index.html b/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.view/index.html deleted file mode 100644 index 6e86d2455..000000000 --- a/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/solution.view/index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - -

Press "Q" and "W" together (can be in any language).

- - - - - - diff --git a/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/task.md b/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/task.md deleted file mode 100644 index 51c2fa2bd..000000000 --- a/2-ui/3-event-details/5-keyboard-events/2-check-sync-keydown/task.md +++ /dev/null @@ -1,19 +0,0 @@ -importance: 5 - ---- - -# Extended hotkeys - -Create a function `runOnKeys(func, code1, code2, ... code_n)` that runs `func` on simultaneous pressing of keys with codes `code1`, `code2`, ..., `code_n`. - -For instance, the code below shows `alert` when `"Q"` and `"W"` are pressed together (in any language, with or without CapsLock) - -```js no-beautify -runOnKeys( - () => alert("Hello!"), - "KeyQ", - "KeyW" -); -``` - -[demo src="solution"] diff --git a/2-ui/3-event-details/5-keyboard-events/article.md b/2-ui/3-event-details/5-keyboard-events/article.md deleted file mode 100644 index e6b4a6137..000000000 --- a/2-ui/3-event-details/5-keyboard-events/article.md +++ /dev/null @@ -1,204 +0,0 @@ -# Keyboard: keydown and keyup - -Before we get to keyboard, please note that on modern devices there are other ways to "input something". For instance, people use speech recognition (especially on mobile devices) or copy/paste with the mouse. - -So if we want to track any input into an `` field, then keyboard events are not enough. There's another event named `input` to track changes of an `` field, by any means. And it may be a better choice for such task. We'll cover it later in the chapter . - -Keyboard events should be used when we want to handle keyboard actions (virtual keyboard also counts). For instance, to react on arrow keys `key:Up` and `key:Down` or hotkeys (including combinations of keys). - - -## Teststand [#keyboard-test-stand] - -```offline -To better understand keyboard events, you can use the [teststand](sandbox:keyboard-dump). -``` - -```online -To better understand keyboard events, you can use the teststand below. - -Try different key combinations in the text field. - -[codetabs src="keyboard-dump" height=480] -``` - - -## Keydown and keyup - -The `keydown` events happens when a key is pressed down, and then `keyup` -- when it's released. - -### event.code and event.key - -The `key` property of the event object allows to get the character, while the `code` property of the event object allows to get the "physical key code". - -For instance, the same key `key:Z` can be pressed with or without `key:Shift`. That gives us two different characters: lowercase `z` and uppercase `Z`. - -The `event.key` is exactly the character, and it will be different. But `event.code` is the same: - -| Key | `event.key` | `event.code` | -|--------------|-------------|--------------| -| `key:Z` |`z` (lowercase) |`KeyZ` | -| `key:Shift+Z`|`Z` (uppercase) |`KeyZ` | - - -If a user works with different languages, then switching to another language would make a totally different character instead of `"Z"`. That will become the value of `event.key`, while `event.code` is always the same: `"KeyZ"`. - -```smart header="\"KeyZ\" and other key codes" -Every key has the code that depends on its location on the keyboard. Key codes described in the [UI Events code specification](https://www.w3.org/TR/uievents-code/). - -For instance: -- Letter keys have codes `"Key"`: `"KeyA"`, `"KeyB"` etc. -- Digit keys have codes: `"Digit"`: `"Digit0"`, `"Digit1"` etc. -- Special keys are coded by their names: `"Enter"`, `"Backspace"`, `"Tab"` etc. - -There are several widespread keyboard layouts, and the specification gives key codes for each of them. - -Read the [alphanumeric section of the spec](https://www.w3.org/TR/uievents-code/#key-alphanumeric-section) for more codes, or just press a key in the [teststand](#keyboard-test-stand) above. -``` - -```warn header="Case matters: `\"KeyZ\"`, not `\"keyZ\"`" -Seems obvious, but people still make mistakes. - -Please evade mistypes: it's `KeyZ`, not `keyZ`. The check like `event.code=="keyZ"` won't work: the first letter of `"Key"` must be uppercase. -``` - -What if a key does not give any character? For instance, `key:Shift` or `key:F1` or others. For those keys, `event.key` is approximately the same as `event.code`: - -| Key | `event.key` | `event.code` | -|--------------|-------------|--------------| -| `key:F1` |`F1` |`F1` | -| `key:Backspace` |`Backspace` |`Backspace` | -| `key:Shift`|`Shift` |`ShiftRight` or `ShiftLeft` | - -Please note that `event.code` specifies exactly which key is pressed. For instance, most keyboards have two `key:Shift` keys: on the left and on the right side. The `event.code` tells us exactly which one was pressed, and `event.key` is responsible for the "meaning" of the key: what it is (a "Shift"). - -Let's say, we want to handle a hotkey: `key:Ctrl+Z` (or `key:Cmd+Z` for Mac). Most text editors hook the "Undo" action on it. We can set a listener on `keydown` and check which key is pressed. - -Please answer the question -- in such a listener, should we check the value of `event.key` or `event.code`? - -<<<<<<< HEAD -Please, pause and answer. -======= -On one hand, the value of `event.key` is a character, it changes depending on the language. If the visitor has several languages in OS and switches between them, the same key gives different characters. So it makes sense to check `event.code`, it's always the same. ->>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 - -Made up your mind? - -If you've got an understanding, then the answer is, of course, `event.code`, as we don't want `event.key` there. The value of `event.key` can change depending on the language or `CapsLock` enabled. The value of `event.code` is strictly bound to the key, so here we go: - -```js run -document.addEventListener('keydown', function(event) { - if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) { - alert('Undo!') - } -}); -``` - -<<<<<<< HEAD -======= -On the other hand, there's a problem with `event.code`. For different keyboard layouts, the same key may have different characters. - -For example, here are US layout ("QWERTY") and German layout ("QWERTZ") under it (from Wikipedia): - -![](us-layout.svg) - -![](german-layout.svg) - -For the same key, US layout has "Z", while German layout has "Y" (letters are swapped). - -Literally, `event.code` will equal `KeyZ` for people with German layout when they press `key:Y`. - -If we check `event.code == 'KeyZ'` in our code, then for people with German layout such test will pass when they press `key:Y`. - -That sounds really odd, but so it is. The [specification](https://www.w3.org/TR/uievents-code/#table-key-code-alphanumeric-writing-system) explicitly mentions such behavior. - -So, `event.code` may match a wrong character for unexpected layout. Same letters in different layouts may map to different physical keys, leading to different codes. Luckily, that happens only with several codes, e.g. `keyA`, `keyQ`, `keyZ` (as we've seen), and doesn't happen with special keys such as `Shift`. You can find the list in the [specification](https://www.w3.org/TR/uievents-code/#table-key-code-alphanumeric-writing-system). - -To reliably track layout-dependent characters, `event.key` may be a better way. - -On the other hand, `event.code` has the benefit of staying always the same, bound to the physical key location, even if the visitor changes languages. So hotkeys that rely on it work well even in case of a language switch. - -Do we want to handle layout-dependant keys? Then `event.key` is the way to go. - -Or we want a hotkey to work even after a language switch? Then `event.code` may be better. - ->>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 -## Auto-repeat - -If a key is being pressed for a long enough time, it starts to repeat: the `keydown` triggers again and again, and then when it's released we finally get `keyup`. So it's kind of normal to have many `keydown` and a single `keyup`. - -For all repeating keys the event object has `event.repeat` property set to `true`. - - -## Default actions - -Default actions vary, as there are many possible things that may be initiated by the keyboard. - -For instance: - -- A character appears on the screen (the most obvious outcome). -- A character is deleted (`key:Delete` key). -- The page is scrolled (`key:PageDown` key). -- The browser opens the "Save Page" dialog (`key:Ctrl+S`) -- ...and so on. - -Preventing the default action on `keydown` can cancel most of them, with the exception of OS-based special keys. For instance, on Windows `key:Alt+F4` closes the current browser window. And there's no way to stop it by preventing the default action in JavaScript. - -For instance, the `` below expects a phone number, so it does not accept keys except digits, `+`, `()` or `-`: - -```html autorun height=60 run - - -``` - -Please note that special keys, such as `key:Backspace`, `key:Left`, `key:Right`, `key:Ctrl+V`, do not work in the input. That's a side-effect of the strict filter `checkPhoneKey`. - -Let's relax it a little bit: - - -```html autorun height=60 run - - -``` - -Now arrows and deletion works well. - -...But we still can enter anything by using a mouse and right-click + Paste. So the filter is not 100% reliable. We can just let it be like that, because most of time it works. Or an alternative approach would be to track the `input` event -- it triggers after any modification. There we can check the new value and highlight/modify it when it's invalid. - -## Legacy - -In the past, there was a `keypress` event, and also `keyCode`, `charCode`, `which` properties of the event object. - -There were so many browser incompatibilities that developers of the specification decided to deprecate all of them. The old code still works, as the browser keep supporting them, but there's totally no need to use those any more. - -There was time when this chapter included their detailed description. But as of now we can forget about those. - -<<<<<<< HEAD - -======= ->>>>>>> 852ee189170d9022f67ab6d387aeae76810b5923 -## Summary - -Pressing a key always generates a keyboard event, be it symbol keys or special keys like `key:Shift` or `key:Ctrl` and so on. The only exception is `key:Fn` key that sometimes presents on a laptop keyboard. There's no keyboard event for it, because it's often implemented on lower level than OS. - -Keyboard events: - -- `keydown` -- on pressing the key (auto-repeats if the key is pressed for long), -- `keyup` -- on releasing the key. - -Main keyboard event properties: - -- `code` -- the "key code" (`"KeyA"`, `"ArrowLeft"` and so on), specific to the physical location of the key on keyboard. -- `key` -- the character (`"A"`, `"a"` and so on), for non-character keys usually has the same value as `code`. - -In the past, keyboard events were sometimes used to track user input in form fields. That's not reliable, because the input can come from various sources. We have `input` and `change` events to handle any input (covered later in the chapter ). They trigger after any input, including mouse or speech recognition. - -We should use keyboard events when we really want keyboard. For example, to react on hotkeys or special keys. diff --git a/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/index.html b/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/index.html deleted file mode 100644 index 401062830..000000000 --- a/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - -
- - Prevent default for: -     - - -

- Ignore: -     - -

- -

Focus on the input field and press a key.

- - - - - -
- - - - - diff --git a/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/script.js b/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/script.js deleted file mode 100644 index 5eba24c7a..000000000 --- a/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/script.js +++ /dev/null @@ -1,28 +0,0 @@ -kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle; - -let lastTime = Date.now(); - -function handle(e) { - if (form.elements[e.type + 'Ignore'].checked) return; - - let text = e.type + - ' key=' + e.key + - ' code=' + e.code + - (e.shiftKey ? ' shiftKey' : '') + - (e.ctrlKey ? ' ctrlKey' : '') + - (e.altKey ? ' altKey' : '') + - (e.metaKey ? ' metaKey' : '') + - (e.repeat ? ' (repeat)' : '') + - "\n"; - - if (area.value && Date.now() - lastTime > 250) { - area.value += new Array(81).join('-') + '\n'; - } - lastTime = Date.now(); - - area.value += text; - - if (form.elements[e.type + 'Stop'].checked) { - e.preventDefault(); - } -} diff --git a/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/style.css b/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/style.css deleted file mode 100644 index 8474832b1..000000000 --- a/2-ui/3-event-details/5-keyboard-events/keyboard-dump.view/style.css +++ /dev/null @@ -1,18 +0,0 @@ -#kinput { - font-size: 150%; - box-sizing: border-box; - width: 95%; -} - -#area { - width: 95%; - box-sizing: border-box; - height: 250px; - border: 1px solid black; - display: block; -} - -form label { - display: inline; - white-space: nowrap; -} \ No newline at end of file