This commit is contained in:
Ilya Kantor 2019-08-18 08:45:16 +03:00
parent cdf6156088
commit 28cc5c82c0
2 changed files with 42 additions and 32 deletions

View file

@ -74,15 +74,19 @@ alert(id.description); // id
Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite. Symbols allow us to create "hidden" properties of an object, that no other part of code can occasionally access or overwrite.
For instance, if we're working with `user` objects, that belong to a third-party code and don't have any `id` field. We'd like to add identifiers to them. For instance, if we're working with `user` objects, that belong to a third-party code. We'd like to add identifiers to them.
Let's use a symbol key for it: Let's use a symbol key for it:
```js run ```js run
let user = { name: "John" }; let user = { // belongs to another code
name: "John"
};
let id = Symbol("id"); let id = Symbol("id");
user[id] = "ID Value"; user[id] = 1;
alert( user[id] ); // we can access the data using the symbol as the key alert( user[id] ); // we can access the data using the symbol as the key
``` ```
@ -108,13 +112,13 @@ There will be no conflict between our and their identifiers, because symbols are
```js run ```js run
let user = { name: "John" }; let user = { name: "John" };
// our script uses "id" property // Our script uses "id" property
user.id = "ID Value"; user.id = "Our id value";
// ...if later another script the uses "id" for its purposes... // ...Another script also wants "id" for its purposes...
user.id = "Their id value" user.id = "Their id value"
// boom! overwritten! it did not mean to harm the colleague, but did it! // Boom! overwritten by another script!
``` ```
### Symbols in a literal ### Symbols in a literal

View file

@ -6,20 +6,19 @@ In the modern HTML standard there's a [section about Drag and Drop](https://html
They are interesting because they allow to solve simple tasks easily, and also allow to handle drag'n'drop of "external" files into the browser. So we can take a file in the OS file-manager and drop it into the browser window. Then JavaScript gains access to its contents. They are interesting because they allow to solve simple tasks easily, and also allow to handle drag'n'drop of "external" files into the browser. So we can take a file in the OS file-manager and drop it into the browser window. Then JavaScript gains access to its contents.
But native Drag Events also have limitations. For instance, we can't limit dragging by a certain area. Also we can't make it "horizontal" or "vertical" only. There are other drag'n'drop tasks that can't be implemented using that API. But native Drag Events also have limitations. For instance, we can't limit dragging by a certain area. Also we can't make it "horizontal" or "vertical" only. There are other drag'n'drop tasks that can't be done using that API.
So here we'll see how to implement Drag'n'Drop using mouse events. Not that hard either. Here we'll see how to implement Drag'n'Drop using mouse events.
## Drag'n'Drop algorithm ## Drag'n'Drop algorithm
The basic Drag'n'Drop algorithm looks like this: The basic Drag'n'Drop algorithm looks like this:
1. Catch `mousedown` on a draggable element. 1. On `mousedown` - prepare the element for moving, if needed (maybe create a copy of it).
2. Prepare the element for moving (maybe create a copy of it or whatever). 2. Then on `mousemove` move it by changing `left/top` and `position:absolute`.
3. Then on `mousemove` move it by changing `left/top` and `position:absolute`. 3. On `mouseup` - perform all actions related to a finished Drag'n'Drop.
4. On `mouseup` (button release) -- perform all actions related to a finished Drag'n'Drop.
These are the basics. We can extend it, for instance, by highlighting droppable (available for the drop) elements when hovering over them. These are the basics. Later we can extend it, for instance, by highlighting droppable (available for the drop) elements when hovering over them.
Here's the algorithm for drag'n'drop of a ball: Here's the algorithm for drag'n'drop of a ball:
@ -32,7 +31,7 @@ ball.onmousedown = function(event) { // (1) start the process
// move it out of any current parents directly into body // move it out of any current parents directly into body
// to make it positioned relative to the body // to make it positioned relative to the body
document.body.append(ball); document.body.append(ball);
// ...and put that absolutely positioned ball under the cursor // ...and put that absolutely positioned ball under the pointer
moveAt(event.pageX, event.pageY); moveAt(event.pageX, event.pageY);
@ -65,7 +64,7 @@ Here's an example in action:
[iframe src="ball" height=230] [iframe src="ball" height=230]
Try to drag'n'drop the mouse and you'll see the strange behavior. Try to drag'n'drop the mouse and you'll see such behavior.
``` ```
That's because the browser has its own Drag'n'Drop for images and some other elements that runs automatically and conflicts with ours. That's because the browser has its own Drag'n'Drop for images and some other elements that runs automatically and conflicts with ours.
@ -88,7 +87,7 @@ In action:
Another important aspect -- we track `mousemove` on `document`, not on `ball`. From the first sight it may seem that the mouse is always over the ball, and we can put `mousemove` on it. Another important aspect -- we track `mousemove` on `document`, not on `ball`. From the first sight it may seem that the mouse is always over the ball, and we can put `mousemove` on it.
But as we remember, `mousemove` triggers often, but not for every pixel. So after swift move the cursor can jump from the ball somewhere in the middle of document (or even outside of the window). But as we remember, `mousemove` triggers often, but not for every pixel. So after swift move the pointer can jump from the ball somewhere in the middle of document (or even outside of the window).
So we should listen on `document` to catch it. So we should listen on `document` to catch it.
@ -101,15 +100,17 @@ ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
ball.style.top = pageY - ball.offsetHeight / 2 + 'px'; ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
``` ```
Not bad, but there's a side-effect. To initiate the drag'n'drop, we can `mousedown` anywhere on the ball. But if do it at the edge, then the ball suddenly "jumps" to become centered. Not bad, but there's a side-effect. To initiate the drag'n'drop, we can `mousedown` anywhere on the ball. But if "take" it from its edge, then the ball suddenly "jumps" to become centered under the mouse pointer.
It would be better if we keep the initial shift of the element relative to the pointer. It would be better if we keep the initial shift of the element relative to the pointer.
For instance, if we start dragging by the edge of the ball, then the cursor should remain over the edge while dragging. For instance, if we start dragging by the edge of the ball, then the pointer should remain over the edge while dragging.
![](ball_shift.svg) ![](ball_shift.svg)
1. When a visitor presses the button (`mousedown`) -- we can remember the distance from the cursor to the left-upper corner of the ball in variables `shiftX/shiftY`. We should keep that distance while dragging. Let's update our algorithm:
1. When a visitor presses the button (`mousedown`) - remember the distance from the pointer to the left-upper corner of the ball in variables `shiftX/shiftY`. We'll keep that distance while dragging.
To get these shifts we can substract the coordinates: To get these shifts we can substract the coordinates:
@ -123,7 +124,7 @@ For instance, if we start dragging by the edge of the ball, then the cursor shou
```js ```js
// onmousemove // onmousemove
// ball has position:absoute // у мяча ball стоит position:absoute
ball.style.left = event.pageX - *!*shiftX*/!* + 'px'; ball.style.left = event.pageX - *!*shiftX*/!* + 'px';
ball.style.top = event.pageY - *!*shiftY*/!* + 'px'; ball.style.top = event.pageY - *!*shiftY*/!* + 'px';
``` ```
@ -177,25 +178,27 @@ In action (inside `<iframe>`):
[iframe src="ball3" height=230] [iframe src="ball3" height=230]
``` ```
The difference is especially noticeable if we drag the ball by its right-bottom corner. In the previous example the ball "jumps" under the pointer. Now it fluently follows the cursor from the current position. The difference is especially noticeable if we drag the ball by its right-bottom corner. In the previous example the ball "jumps" under the pointer. Now it fluently follows the pointer from the current position.
## Potential drop targets (droppables) ## Potential drop targets (droppables)
In previous examples the ball could be dropped just "anywhere" to stay. In real-life we usually take one element and drop it onto another. For instance, a file into a folder, or a user into a trash can or whatever. In previous examples the ball could be dropped just "anywhere" to stay. In real-life we usually take one element and drop it onto another. For instance, a "file" into a "folder" or something else.
In other words, we take a "draggable" element and drop it onto "droppable" element. Speaking abstract, we take a "draggable" element and drop it onto "droppable" element.
We need to know where the element was dropped at the end of Drag'n'Drop -- to do the corresponding action, and, preferably, know the droppable we're dragging over, to highlight it. We need to know:
- where the element was dropped at the end of Drag'n'Drop -- to do the corresponding action,
- and, preferably, know the droppable we're dragging over, to highlight it.
The solution is kind-of interesting and just a little bit tricky, so let's cover it here. The solution is kind-of interesting and just a little bit tricky, so let's cover it here.
What may be the first idea? Probably to set `mouseover/mouseup` handlers on potential droppables and detect when the mouse pointer appears over them. And then we know that we are dragging over/dropping on that element. What may be the first idea? Probably to set `mouseover/mouseup` handlers on potential droppables?
But that doesn't work. But that doesn't work.
The problem is that, while we're dragging, the draggable element is always above other elements. And mouse events only happen on the top element, not on those below it. The problem is that, while we're dragging, the draggable element is always above other elements. And mouse events only happen on the top element, not on those below it.
For instance, below are two `<div>` elements, red on top of blue. There's no way to catch an event on the blue one, because the red is on top: For instance, below are two `<div>` elements, red one on top of the blue one (fully covers). There's no way to catch an event on the blue one, because the red is on top:
```html run autorun height=60 ```html run autorun height=60
<style> <style>
@ -218,24 +221,27 @@ So, what to do?
There's a method called `document.elementFromPoint(clientX, clientY)`. It returns the most nested element on given window-relative coordinates (or `null` if given coordinates are out of the window). There's a method called `document.elementFromPoint(clientX, clientY)`. It returns the most nested element on given window-relative coordinates (or `null` if given coordinates are out of the window).
So in any of our mouse event handlers we can detect the potential droppable under the pointer like this: We can use it in any of our mouse event handlers to detect the potential droppable under the pointer, like this:
```js ```js
// in a mouse event handler // in a mouse event handler
ball.hidden = true; // (*) ball.hidden = true; // (*) hide the element that we drag
let elemBelow = document.elementFromPoint(event.clientX, event.clientY); let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
ball.hidden = false;
// elemBelow is the element below the ball, may be droppable // elemBelow is the element below the ball, may be droppable
ball.hidden = false;
``` ```
Please note: we need to hide the ball before the call `(*)`. Otherwise we'll usually have a ball on these coordinates, as it's the top element under the pointer: `elemBelow=ball`. Please note: we need to hide the ball before the call `(*)`. Otherwise we'll usually have a ball on these coordinates, as it's the top element under the pointer: `elemBelow=ball`. So we hide it and immediately show again.
We can use that code to check what element we're "flying over" at any time. And handle the drop when it happens. We can use that code to check what element we're "flying over" at any time. And handle the drop when it happens.
An extended code of `onMouseMove` to find "droppable" elements: An extended code of `onMouseMove` to find "droppable" elements:
```js ```js
let currentDroppable = null; // potential droppable that we're flying over right now // potential droppable that we're flying over right now
let currentDroppable = null;
function onMouseMove(event) { function onMouseMove(event) {
moveAt(event.pageX, event.pageY); moveAt(event.pageX, event.pageY);