pointer events improvements

This commit is contained in:
Ilya Kantor 2021-07-24 15:01:36 +03:00
parent 1b1a2c4b66
commit 9c5388cd57
2 changed files with 83 additions and 36 deletions

View file

@ -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`.
- 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,12 +216,20 @@ Here's the essential code:
thumb.onpointerdown = function(event) {
// retarget all pointer events (until pointerup) to thumb
thumb.setPointerCapture(event.pointerId);
};
thumb.onpointermove = function(event) {
// 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
};
};
// note: no need to call thumb.releasePointerCapture,
@ -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`.

View file

@ -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>