Skip to content

Commit 9c5388c

Browse files
committed
pointer events improvements
1 parent 1b1a2c4 commit 9c5388c

File tree

2 files changed

+82
-35
lines changed

2 files changed

+82
-35
lines changed

2-ui/3-event-details/6-pointer-events/article.md

+67-33
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ Let's make a small overview, so that you understand the general picture and the
99
- Long ago, in the past, there were only mouse events.
1010

1111
Then touch devices became widespread, phones and tablets in particular. For the existing scripts to work, they generated (and still generate) mouse events. For instance, tapping a touchscreen generates `mousedown`. So touch devices worked well with web pages.
12-
12+
1313
But touch devices have more capabilities than a mouse. For example, it's possible to touch multiple points at once ("multi-touch"). Although, mouse events don't have necessary properties to handle such multi-touches.
1414

1515
- So touch events were introduced, such as `touchstart`, `touchend`, `touchmove`, that have touch-specific properties (we don't cover them in detail here, because pointer events are even better).
1616

17-
Still, it wasn't enough, as there are many other devices, such as pens, that have their own features. Also, writing code that listens for both touch and mouse events was cumbersome.
17+
Still, it wasn't enough, as there are many other devices, such as pens, that have their own features. Also, writing code that listens for both touch and mouse events was cumbersome.
1818

1919
- To solve these issues, the new standard Pointer Events was introduced. It provides a single set of events for all kinds of pointing devices.
2020

21-
As of now, [Pointer Events Level 2](https://www.w3.org/TR/pointerevents2/) specification is supported in all major browsers, while the newer [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works and is mostly compatible with Pointer Events level 2.
21+
As of now, [Pointer Events Level 2](https://www.w3.org/TR/pointerevents2/) specification is supported in all major browsers, while the newer [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works and is mostly compatible with Pointer Events level 2.
2222

2323
Unless you develop for old browsers, such as Internet Explorer 10, or for Safari 12 or below, there's no point in using mouse or touch events any more -- we can switch to pointer events.
2424

@@ -43,29 +43,29 @@ Pointer events are named similarly to mouse events:
4343
| `gotpointercapture` | - |
4444
| `lostpointercapture` | - |
4545

46-
As we can see, for every `mouse<event>`, there's a `pointer<event>` that plays a similar role. Also there are 3 additional pointer events that don't have a corresponding `mouse...` counterpart, we'll explain them soon.
46+
As we can see, for every `mouse<event>`, there's a `pointer<event>` that plays a similar role. Also there are 3 additional pointer events that don't have a corresponding `mouse...` counterpart, we'll explain them soon.
4747

4848
```smart header="Replacing `mouse<event>` with `pointer<event>` in our code"
4949
We can replace `mouse<event>` events with `pointer<event>` in our code and expect things to continue working fine with mouse.
5050

51-
The support for touch devices will also "magically" improve. Although, we may need to add `touch-action: none` in some places in CSS. We'll cover it below in the section about `pointercancel`.
51+
The support for touch devices will also "magically" improve. Although, we may need to add `touch-action: none` in some places in CSS. We'll cover it below in the section about `pointercancel`.
5252
```
5353
5454
## Pointer event properties
5555
5656
Pointer events have the same properties as mouse events, such as `clientX/Y`, `target`, etc., plus some others:
5757
5858
- `pointerId` - the unique identifier of the pointer causing the event.
59-
59+
6060
Browser-generated. Allows us to handle multiple pointers, such as a touchscreen with stylus and multi-touch (examples will follow).
61-
- `pointerType` - the pointing device type. Must be a string, one of: "mouse", "pen" or "touch".
61+
- `pointerType` - the pointing device type. Must be a string, one of: "mouse", "pen" or "touch".
6262
6363
We can use this property to react differently on various pointer types.
6464
- `isPrimary` - is `true` for the primary pointer (the first finger in multi-touch).
6565
6666
Some pointer devices measure contact area and pressure, e.g. for a finger on the touchscreen, there are additional properties for that:
6767
68-
- `width` - the width of the area where the pointer (e.g. a finger) touches the device. Where unsupported, e.g. for a mouse, it's always `1`.
68+
- `width` - the width of the area where the pointer (e.g. a finger) touches the device. Where unsupported, e.g. for a mouse, it's always `1`.
6969
- `height` - the height of the area where the pointer touches the device. Where unsupported, it's always `1`.
7070
- `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`.
7171
- `tangentialPressure` - the normalized tangential pressure.
@@ -102,11 +102,11 @@ Please note: you must be using a touchscreen device, such as a phone or a tablet
102102

103103
## Event: pointercancel
104104

105-
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.
105+
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.
106106

107-
Such causes are:
107+
Such causes are:
108108
- The pointer device hardware was physically disabled.
109-
- The device orientation changed (tablet rotated).
109+
- The device orientation changed (tablet rotated).
110110
- The browser decided to handle the interaction on its own, considering it a mouse gesture or zoom-and-pan action or something else.
111111

112112
We'll demonstrate `pointercancel` on a practical example to see how it affects us.
@@ -126,7 +126,7 @@ Here is the flow of user actions and the corresponding events:
126126
So the issue is that the browser "hijacks" the interaction: `pointercancel` fires in the beginning of the "drag-and-drop" process, and no more `pointermove` events are generated.
127127

128128
```online
129-
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
129+
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
130130
131131
[iframe src="ball" height=240 edit]
132132
```
@@ -141,7 +141,7 @@ We need to do two things:
141141
- We can do this by setting `ball.ondragstart = () => false`, just as described in the article <info:mouse-drag-and-drop>.
142142
- That works well for mouse events.
143143
2. For touch devices, there are other touch-related browser actions (besides drag'n'drop). To avoid problems with them too:
144-
- Prevent them by setting `#ball { touch-action: none }` in CSS.
144+
- Prevent them by setting `#ball { touch-action: none }` in CSS.
145145
- Then our code will start working on touch devices.
146146

147147
After we do that, the events will work as intended, the browser won't hijack the process and doesn't emit `pointercancel`.
@@ -163,7 +163,7 @@ Pointer capturing is a special feature of pointer events.
163163
The idea is very simple, but may seem quite odd at first, as nothing like that exists for any other event type.
164164

165165
The main method is:
166-
- `elem.setPointerCapture(pointerId)` - binds events with the given `pointerId` to `elem`. After the call all pointer events with the same `pointerId` will have `elem` as the target (as if happened on `elem`), no matter where in document they really happened.
166+
- `elem.setPointerCapture(pointerId)` -- binds events with the given `pointerId` to `elem`. After the call all pointer events with the same `pointerId` will have `elem` as the target (as if happened on `elem`), no matter where in document they really happened.
167167

168168
In other words, `elem.setPointerCapture(pointerId)` retargets all subsequent events with the given `pointerId` to `elem`.
169169

@@ -172,61 +172,95 @@ The binding is removed:
172172
- automatically when `elem` is removed from the document,
173173
- when `elem.releasePointerCapture(pointerId)` is called.
174174

175+
Now what is it good for? It's time to see a real-life example.
176+
175177
**Pointer capturing can be used to simplify drag'n'drop kind of interactions.**
176178

177-
As an example, let's recall how one can implement a custom slider, described in the <info:mouse-drag-and-drop>.
179+
Let's recall how one can implement a custom slider, described in the <info:mouse-drag-and-drop>.
180+
181+
We can make a `slider` element to represent the strip and the "runner" (`thumb`) inside it:
182+
183+
```html
184+
<div class="slider">
185+
<div class="thumb"></div>
186+
</div>
187+
```
188+
189+
With styles, it looks like this:
190+
191+
[iframe src="slider-html" height=40 edit]
178192

179-
We make a slider element with the strip and the "runner" (`thumb`) inside it.
193+
<p></p>
180194

181-
Then it works like this:
195+
And here's the working logic, as it was described, after replacing mouse events with similar pointer events:
182196

183-
1. The user presses on the slider `thumb` - `pointerdown` triggers.
184-
2. Then they move the pointer - `pointermove` triggers, and we move the `thumb` along.
185-
- ...As the pointer moves, it may leave the slider `thumb`: go above or below it. The `thumb` should move strictly horizontally, remaining aligned with the pointer.
197+
1. The user presses on the slider `thumb` -- `pointerdown` triggers.
198+
2. Then they move the pointer -- `pointermove` triggers, and our code moves the `thumb` element along.
199+
- ...As the pointer moves, it may leave the slider `thumb` element, go above or below it. The `thumb` should move strictly horizontally, remaining aligned with the pointer.
186200

187-
So, to track all pointer movements, including when it goes above/below the `thumb`, we had to assign `pointermove` event handler on the whole `document`.
201+
In the mouse event based solution, to track all pointer movements, including when it goes above/below the `thumb`, we had to assign `mousemove` event handler on the whole `document`.
188202

189-
That solution looks a bit "dirty". One of the problems is that pointer movements around the document may cause side effects, trigger other event handlers, totally not related to the slider.
203+
That's not a cleanest solution, though. One of the problems is that when a user moves the pointer around the document, it may trigger event handlers (such as `mouseover`) on some other elements, invoke totally unrelated UI functionality, and we don't want that.
190204

191-
Pointer capturing provides a means to bind `pointermove` to `thumb` and avoid any such problems:
205+
This is the place where `setPointerCapture` comes into play.
192206

193207
- We can call `thumb.setPointerCapture(event.pointerId)` in `pointerdown` handler,
194-
- Then future pointer events until `pointerup/cancel` will be retargeted to `thumb`.
208+
- Then future pointer events until `pointerup/cancel` will be retargeted to `thumb`.
195209
- When `pointerup` happens (dragging complete), the binding is removed automatically, we don't need to care about it.
196210

197-
So, even if the user moves the pointer around the whole document, events handlers will be called on `thumb`. Besides, coordinate properties of the event objects, such as `clientX/clientY` will still be correct - the capturing only affects `target/currentTarget`.
211+
So, even if the user moves the pointer around the whole document, events handlers will be called on `thumb`. Nevertheless, coordinate properties of the event objects, such as `clientX/clientY` will still be correct - the capturing only affects `target/currentTarget`.
198212

199213
Here's the essential code:
200214

201215
```js
202216
thumb.onpointerdown = function(event) {
203217
// retarget all pointer events (until pointerup) to thumb
204218
thumb.setPointerCapture(event.pointerId);
205-
};
206219

207-
thumb.onpointermove = function(event) {
208-
// moving the slider: listen on the thumb, as all pointer events are retargeted to it
209-
let newLeft = event.clientX - slider.getBoundingClientRect().left;
210-
thumb.style.left = newLeft + 'px';
220+
// start tracking pointer moves
221+
thumb.onpointermove = function(event) {
222+
// moving the slider: listen on the thumb, as all pointer events are retargeted to it
223+
let newLeft = event.clientX - slider.getBoundingClientRect().left;
224+
thumb.style.left = newLeft + 'px';
225+
};
226+
227+
// on pointer up finish tracking pointer moves
228+
thumb.onpointerup = function(event) {
229+
thumb.onpointermove = null;
230+
thumb.onpointerup = null;
231+
// ...also process the "drag end" if needed
232+
};
211233
};
212234

213-
// note: no need to call thumb.releasePointerCapture,
235+
// note: no need to call thumb.releasePointerCapture,
214236
// it happens on pointerup automatically
215237
```
216238

217239
```online
218240
The full demo:
219241
220242
[iframe src="slider" height=100 edit]
243+
244+
<p></p>
245+
246+
In the demo, there's also an additional element with `onmouseover` handler showing the current date.
247+
248+
Please note: while you're dragging the thumb, you may hover over this element, and its handler *does not* trigger.
249+
250+
So the dragging is now free of side effects, thanks to `setPointerCapture`.
221251
```
222252

253+
254+
223255
At the end, pointer capturing gives us two benefits:
224256
1. The code becomes cleaner as we don't need to add/remove handlers on the whole `document` any more. The binding is released automatically.
225-
2. If there are any `pointermove` handlers in the document, they won't be accidentally triggered by the pointer while the user is dragging the slider.
257+
2. If there are other pointer event handlers in the document, they won't be accidentally triggered by the pointer while the user is dragging the slider.
226258

227259
### Pointer capturing events
228260

229-
There are two associated pointer events:
261+
There's one more thing to mention here, for the sake of completeness.
262+
263+
There are two events associated with pointer capturing:
230264

231265
- `gotpointercapture` fires when an element uses `setPointerCapture` to enable capturing.
232266
- `lostpointercapture` fires when the capture is released: either explicitly with `releasePointerCapture` call, or automatically on `pointerup`/`pointercancel`.

2-ui/3-event-details/6-pointer-events/slider.view/index.html

+15-2
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,30 @@
55
<div class="thumb"></div>
66
</div>
77

8+
<p style="border:1px solid gray" onmousemove="this.textContent = new Date()">Mouse over here to see the date</p>
9+
810
<script>
911
let thumb = slider.querySelector('.thumb');
1012
let shiftX;
1113

12-
thumb.onpointerdown = function(event) {
14+
function onThumbDown(event) {
1315
event.preventDefault(); // prevent selection start (browser action)
1416

1517
shiftX = event.clientX - thumb.getBoundingClientRect().left;
1618

1719
thumb.setPointerCapture(event.pointerId);
20+
21+
thumb.onpointermove = onThumbMove;
22+
23+
thumb.onpointerup = event => {
24+
// dragging finished, no need to track pointer any more
25+
// ...any other "drag end" logic here...
26+
thumb.onpointermove = null;
27+
thumb.onpointerup = null;
28+
}
1829
};
1930

20-
thumb.onpointermove = function(event) {
31+
function onThumbMove(event) {
2132
let newLeft = event.clientX - shiftX - slider.getBoundingClientRect().left;
2233

2334
// if the pointer is out of slider => adjust left to be within the bounaries
@@ -32,6 +43,8 @@
3243
thumb.style.left = newLeft + 'px';
3344
};
3445

46+
thumb.onpointerdown = onThumbDown;
47+
3548
thumb.ondragstart = () => false;
3649

3750
</script>

0 commit comments

Comments
 (0)