pointer events improvements
This commit is contained in:
parent
1b1a2c4b66
commit
9c5388cd57
2 changed files with 83 additions and 36 deletions
|
@ -9,16 +9,16 @@ Let's make a small overview, so that you understand the general picture and the
|
|||
- Long ago, in the past, there were only mouse events.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
- 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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
- 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 newer [Pointer Events Level 3](https://w3c.github.io/pointerevents/) is in the works and is mostly compatible with Pointer Events level 2.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -43,12 +43,12 @@ Pointer events are named similarly to mouse events:
|
|||
| `gotpointercapture` | - |
|
||||
| `lostpointercapture` | - |
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
```smart header="Replacing `mouse<event>` with `pointer<event>` in our code"
|
||||
We can replace `mouse<event>` events with `pointer<event>` in our code and expect things to continue working fine with mouse.
|
||||
|
||||
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`.
|
||||
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`.
|
||||
```
|
||||
|
||||
## Pointer event properties
|
||||
|
@ -56,16 +56,16 @@ The support for touch devices will also "magically" improve. Although, we may ne
|
|||
Pointer events have the same properties as mouse events, such as `clientX/Y`, `target`, etc., plus some others:
|
||||
|
||||
- `pointerId` - the unique identifier of the pointer causing the event.
|
||||
|
||||
|
||||
Browser-generated. Allows us to handle multiple pointers, such as a touchscreen with stylus and multi-touch (examples will follow).
|
||||
- `pointerType` - the pointing device type. Must be a string, one of: "mouse", "pen" or "touch".
|
||||
- `pointerType` - the pointing device type. Must be a string, one of: "mouse", "pen" or "touch".
|
||||
|
||||
We can use this property to react differently on various pointer types.
|
||||
- `isPrimary` - is `true` for the primary pointer (the first finger in multi-touch).
|
||||
|
||||
Some pointer devices measure contact area and pressure, e.g. for a finger on the touchscreen, there are additional properties for that:
|
||||
|
||||
- `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`.
|
||||
- `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`.
|
||||
- `height` - the height of the area where the pointer touches the device. Where unsupported, it's always `1`.
|
||||
- `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.
|
||||
|
@ -102,11 +102,11 @@ Please note: you must be using a touchscreen device, such as a phone or a tablet
|
|||
|
||||
## Event: pointercancel
|
||||
|
||||
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.
|
||||
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:
|
||||
Such causes are:
|
||||
- The pointer device hardware was physically disabled.
|
||||
- The device orientation changed (tablet rotated).
|
||||
- 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.
|
||||
|
||||
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:
|
|||
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.
|
||||
|
||||
```online
|
||||
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
|
||||
Here's the drag'n'drop demo with loggin of pointer events (only `up/down`, `move` and `cancel`) in the `textarea`:
|
||||
|
||||
[iframe src="ball" height=240 edit]
|
||||
```
|
||||
|
@ -141,7 +141,7 @@ We need to do two things:
|
|||
- We can do this by setting `ball.ondragstart = () => false`, just as described in the article <info:mouse-drag-and-drop>.
|
||||
- That works well for mouse events.
|
||||
2. For touch devices, there are other touch-related browser actions (besides drag'n'drop). To avoid problems with them too:
|
||||
- Prevent them by setting `#ball { touch-action: none }` in CSS.
|
||||
- Prevent them by setting `#ball { touch-action: none }` in CSS.
|
||||
- Then our code will start working on touch devices.
|
||||
|
||||
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.
|
|||
The idea is very simple, but may seem quite odd at first, as nothing like that exists for any other event type.
|
||||
|
||||
The main method is:
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
In other words, `elem.setPointerCapture(pointerId)` retargets all subsequent events with the given `pointerId` to `elem`.
|
||||
|
||||
|
@ -172,29 +172,43 @@ The binding is removed:
|
|||
- automatically when `elem` is removed from the document,
|
||||
- when `elem.releasePointerCapture(pointerId)` is called.
|
||||
|
||||
Now what is it good for? It's time to see a real-life example.
|
||||
|
||||
**Pointer capturing can be used to simplify drag'n'drop kind of interactions.**
|
||||
|
||||
As an example, let's recall how one can implement a custom slider, described in the <info:mouse-drag-and-drop>.
|
||||
Let's recall how one can implement a custom slider, described in the <info:mouse-drag-and-drop>.
|
||||
|
||||
We make a slider element with the strip and the "runner" (`thumb`) inside it.
|
||||
We can make a `slider` element to represent the strip and the "runner" (`thumb`) inside it:
|
||||
|
||||
Then it works like this:
|
||||
```html
|
||||
<div class="slider">
|
||||
<div class="thumb"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
1. The user presses on the slider `thumb` - `pointerdown` triggers.
|
||||
2. Then they move the pointer - `pointermove` triggers, and we move the `thumb` along.
|
||||
- ...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.
|
||||
With styles, it looks like this:
|
||||
|
||||
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`.
|
||||
[iframe src="slider-html" height=40 edit]
|
||||
|
||||
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.
|
||||
<p></p>
|
||||
|
||||
Pointer capturing provides a means to bind `pointermove` to `thumb` and avoid any such problems:
|
||||
And here's the working logic, as it was described, after replacing mouse events with similar pointer events:
|
||||
|
||||
1. The user presses on the slider `thumb` -- `pointerdown` triggers.
|
||||
2. Then they move the pointer -- `pointermove` triggers, and our code moves the `thumb` element along.
|
||||
- ...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.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
This is the place where `setPointerCapture` comes into play.
|
||||
|
||||
- We can call `thumb.setPointerCapture(event.pointerId)` in `pointerdown` handler,
|
||||
- Then future pointer events until `pointerup/cancel` will be retargeted to `thumb`.
|
||||
- Then future pointer events until `pointerup/cancel` will be retargeted to `thumb`.
|
||||
- When `pointerup` happens (dragging complete), the binding is removed automatically, we don't need to care about it.
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
Here's the essential code:
|
||||
|
||||
|
@ -202,15 +216,23 @@ Here's the essential code:
|
|||
thumb.onpointerdown = function(event) {
|
||||
// retarget all pointer events (until pointerup) to thumb
|
||||
thumb.setPointerCapture(event.pointerId);
|
||||
|
||||
// start tracking pointer moves
|
||||
thumb.onpointermove = function(event) {
|
||||
// moving the slider: listen on the thumb, as all pointer events are retargeted to it
|
||||
let newLeft = event.clientX - slider.getBoundingClientRect().left;
|
||||
thumb.style.left = newLeft + 'px';
|
||||
};
|
||||
|
||||
// on pointer up finish tracking pointer moves
|
||||
thumb.onpointerup = function(event) {
|
||||
thumb.onpointermove = null;
|
||||
thumb.onpointerup = null;
|
||||
// ...also process the "drag end" if needed
|
||||
};
|
||||
};
|
||||
|
||||
thumb.onpointermove = function(event) {
|
||||
// moving the slider: listen on the thumb, as all pointer events are retargeted to it
|
||||
let newLeft = event.clientX - slider.getBoundingClientRect().left;
|
||||
thumb.style.left = newLeft + 'px';
|
||||
};
|
||||
|
||||
// note: no need to call thumb.releasePointerCapture,
|
||||
// note: no need to call thumb.releasePointerCapture,
|
||||
// it happens on pointerup automatically
|
||||
```
|
||||
|
||||
|
@ -218,15 +240,27 @@ thumb.onpointermove = function(event) {
|
|||
The full demo:
|
||||
|
||||
[iframe src="slider" height=100 edit]
|
||||
|
||||
<p></p>
|
||||
|
||||
In the demo, there's also an additional element with `onmouseover` handler showing the current date.
|
||||
|
||||
Please note: while you're dragging the thumb, you may hover over this element, and its handler *does not* trigger.
|
||||
|
||||
So the dragging is now free of side effects, thanks to `setPointerCapture`.
|
||||
```
|
||||
|
||||
|
||||
|
||||
At the end, pointer capturing gives us two benefits:
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
### Pointer capturing events
|
||||
|
||||
There are two associated pointer events:
|
||||
There's one more thing to mention here, for the sake of completeness.
|
||||
|
||||
There are two events associated with pointer capturing:
|
||||
|
||||
- `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`.
|
||||
|
|
|
@ -5,19 +5,30 @@
|
|||
<div class="thumb"></div>
|
||||
</div>
|
||||
|
||||
<p style="border:1px solid gray" onmousemove="this.textContent = new Date()">Mouse over here to see the date</p>
|
||||
|
||||
<script>
|
||||
let thumb = slider.querySelector('.thumb');
|
||||
let shiftX;
|
||||
|
||||
thumb.onpointerdown = function(event) {
|
||||
function onThumbDown(event) {
|
||||
event.preventDefault(); // prevent selection start (browser action)
|
||||
|
||||
shiftX = event.clientX - thumb.getBoundingClientRect().left;
|
||||
|
||||
thumb.setPointerCapture(event.pointerId);
|
||||
|
||||
thumb.onpointermove = onThumbMove;
|
||||
|
||||
thumb.onpointerup = event => {
|
||||
// dragging finished, no need to track pointer any more
|
||||
// ...any other "drag end" logic here...
|
||||
thumb.onpointermove = null;
|
||||
thumb.onpointerup = null;
|
||||
}
|
||||
};
|
||||
|
||||
thumb.onpointermove = function(event) {
|
||||
function onThumbMove(event) {
|
||||
let newLeft = event.clientX - shiftX - slider.getBoundingClientRect().left;
|
||||
|
||||
// if the pointer is out of slider => adjust left to be within the bounaries
|
||||
|
@ -32,6 +43,8 @@
|
|||
thumb.style.left = newLeft + 'px';
|
||||
};
|
||||
|
||||
thumb.onpointerdown = onThumbDown;
|
||||
|
||||
thumb.ondragstart = () => false;
|
||||
|
||||
</script>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue