up
This commit is contained in:
parent
497c9fd2d3
commit
7b3f4550fd
2 changed files with 163 additions and 55 deletions
|
@ -77,24 +77,10 @@ If we omit `return false`, then after our code executes the browser will do its
|
||||||
|
|
||||||
By the way, using event delegation here makes our menu flexible. We can add nested lists and style them using CSS to "slide down".
|
By the way, using event delegation here makes our menu flexible. We can add nested lists and style them using CSS to "slide down".
|
||||||
|
|
||||||
## Other browser actions
|
|
||||||
|
|
||||||
There are many default browser actions.
|
## Prevent futher events
|
||||||
|
|
||||||
Here are more events that cause browser actions:
|
Certain events flow one into another. If we prevent the first event, there will be no second.
|
||||||
|
|
||||||
- `mousedown` -- starts the selection (move the mouse to select).
|
|
||||||
- `click` on `<input type="checkbox">` -- checks/unchecks the `input`.
|
|
||||||
- `submit` -- clicking an `<input type="submit">` or hitting `key:Enter` inside a form field causes this event to happen, and the browser submits the form after it.
|
|
||||||
- `wheel` -- rolling a mouse wheel event has scrolling as the default action.
|
|
||||||
- `keydown` -- pressing a key may lead to adding a character into a field, or other actions.
|
|
||||||
- `contextmenu` -- the event happens on a right-click, the action is to show the browser context menu.
|
|
||||||
- ...
|
|
||||||
|
|
||||||
All the default actions can be prevented if we want to handle the event exclusively by JavaScript.
|
|
||||||
|
|
||||||
````warn header="Events may be related"
|
|
||||||
Certain events flow one into another.
|
|
||||||
|
|
||||||
For instance, `mousedown` on an `<input>` field leads to focusing in it, and the `focus` event. If we prevent the `mousedown` event, there's no focus.
|
For instance, `mousedown` on an `<input>` field leads to focusing in it, and the `focus` event. If we prevent the `mousedown` event, there's no focus.
|
||||||
|
|
||||||
|
@ -107,10 +93,129 @@ But if you click the second one, there's no focus.
|
||||||
<input *!*onmousedown="return false"*/!* onfocus="this.value=''" value="Click me">
|
<input *!*onmousedown="return false"*/!* onfocus="this.value=''" value="Click me">
|
||||||
```
|
```
|
||||||
|
|
||||||
That's because the browser action is canceled on `mousedown`. The focusing is still possible if we use another way to enter the input. For instance, the `key:Tab` key to switch from the 1st input into the 2nd.
|
That's because the browser action is canceled on `mousedown`. The focusing is still possible if we use another way to enter the input. For instance, the `key:Tab` key to switch from the 1st input into the 2nd. But not with the mouse click any more.
|
||||||
````
|
|
||||||
|
|
||||||
|
## event.defaultPrevented
|
||||||
|
|
||||||
|
The property `event.defaultPrevented` is `true` if the default action was prevented, and `false` otherwise.
|
||||||
|
|
||||||
|
There's an interesting use case for it.
|
||||||
|
|
||||||
|
You remember in the chapter <info:bubbling-and-capturing> we talked about `event.stopPropagation()` and why stopping bubbling is bad?
|
||||||
|
|
||||||
|
Sometimes we can use `event.defaultPrevented` instead.
|
||||||
|
|
||||||
|
Let's see a practical example where stopping the bubbling looks necessary, but actually we can do well without it.
|
||||||
|
|
||||||
|
By default the browser on `contextmenu` event (right mouse click) shows a context menu with standard options. We can prevent it and show our own, like this:
|
||||||
|
|
||||||
|
```html autorun height=50 no-beautify run
|
||||||
|
<button>Right-click for browser context menu</button>
|
||||||
|
|
||||||
|
<button *!*oncontextmenu="alert('Draw our menu'); return false"*/!*>
|
||||||
|
Right-click for our context menu
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's say we want to implement our own document-wide context menu, with our options. And inside the document we may have other elements with their own context menus:
|
||||||
|
|
||||||
|
```html autorun height=80 no-beautify run
|
||||||
|
<p>Right-click here for the document context menu</p>
|
||||||
|
<button id="elem">Right-click here for the button context menu</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
elem.oncontextmenu = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert("Button context menu");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.oncontextmenu = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert("Document context menu");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The problem is that when we click on `elem`, we get two menus: the button-level and (the event bubbles up) the document-level menu.
|
||||||
|
|
||||||
|
How to fix it? One of solutions is to think like: "We fully handle the event in the button handler, let's stop it" and use `event.stopPropagation()`:
|
||||||
|
|
||||||
|
```html autorun height=80 no-beautify run
|
||||||
|
<p>Right-click for the document menu</p>
|
||||||
|
<button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
elem.oncontextmenu = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
*!*
|
||||||
|
event.stopPropagation();
|
||||||
|
*/!*
|
||||||
|
alert("Button context menu");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.oncontextmenu = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert("Document context menu");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the button-level menu works as intended. But the price is high. We forever deny access to information about right-clicks for any outer code, including counters that gather statistics and so on. That's quite unwise.
|
||||||
|
|
||||||
|
An alternative solution would be to check in the `document` handler if the default action was prevented? If it is so, then the event was handled, and we don't need to react on it.
|
||||||
|
|
||||||
|
|
||||||
|
```html autorun height=80 no-beautify run
|
||||||
|
<p>Right-click for the document menu (fixed with event.defaultPrevented)</p>
|
||||||
|
<button id="elem">Right-click for the button menu</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
elem.oncontextmenu = function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert("Button context menu");
|
||||||
|
};
|
||||||
|
|
||||||
|
document.oncontextmenu = function(event) {
|
||||||
|
*!*
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
alert("Document context menu");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now everything also works correctly. If we have nested elements, and each of them has a context menu of its own, that would also work. Just make sure to check for `event.defaultPrevented` in each `contextmenu` handler.
|
||||||
|
|
||||||
|
```smart header="event.stopPropagation() and event.preventDefault()"
|
||||||
|
As we can clearly see, `event.stopPropagation()` and `event.preventDefault()` (also known as `return false`) are two different things. They are not related to each other.
|
||||||
|
```
|
||||||
|
|
||||||
|
```smart header="Nested context menus architecture"
|
||||||
|
There are also alternative ways to implement nested context menus. One of them is to have a special global object with a method that handles `document.oncontextmenu`, and also methods that allow to store various "lower-level" handlers in it.
|
||||||
|
|
||||||
|
The object will catch any right-click, look through stored handlers and run the appropriate one.
|
||||||
|
|
||||||
|
But then each piece of code that wants a context menu should know about that object and use its help instead of the own `contextmenu` handler.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- Browser has default actions for many events -- following a link, submitting a form etc.
|
There are many default browser actions:
|
||||||
- To prevent a default action -- use either `event.preventDefault()` or `return false`. The second method works only for handlers assigned with `on<event>`.
|
|
||||||
|
- `mousedown` -- starts the selection (move the mouse to select).
|
||||||
|
- `click` on `<input type="checkbox">` -- checks/unchecks the `input`.
|
||||||
|
- `submit` -- clicking an `<input type="submit">` or hitting `key:Enter` inside a form field causes this event to happen, and the browser submits the form after it.
|
||||||
|
- `wheel` -- rolling a mouse wheel event has scrolling as the default action.
|
||||||
|
- `keydown` -- pressing a key may lead to adding a character into a field, or other actions.
|
||||||
|
- `contextmenu` -- the event happens on a right-click, the action is to show the browser context menu.
|
||||||
|
- ...there are more...
|
||||||
|
|
||||||
|
All the default actions can be prevented if we want to handle the event exclusively by JavaScript.
|
||||||
|
|
||||||
|
To prevent a default action -- use either `event.preventDefault()` or `return false`. The second method works only for handlers assigned with `on<event>`.
|
||||||
|
|
||||||
|
If the default action was prevented, the value of `event.defaultPrevented` becomes `true`, otherwise it's `false`.
|
||||||
|
|
|
@ -15,19 +15,19 @@ Events form a hierarchy, just like DOM element classes. The root is the built-in
|
||||||
We can create `Event` objects like this:
|
We can create `Event` objects like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let event = new Event(event type[, flags]);
|
let event = new Event(event type[, options]);
|
||||||
```
|
```
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|
||||||
- *event type* -- may be any string, like `"click"` or our own like `"hey-ho!"`.
|
- *event type* -- may be any string, like `"click"` or our own like `"hey-ho!"`.
|
||||||
- *flags* -- the object with two optional properties:
|
- *options* -- the object with two optional properties:
|
||||||
- `bubbles: true/false` -- if `true`, then the event bubbles.
|
- `bubbles: true/false` -- if `true`, then the event bubbles.
|
||||||
- `cancelable: true/false` -- if `true`, then the "default action" may be prevented. Later we'll see what it means for custom events.
|
- `cancelable: true/false` -- if `true`, then the "default action" may be prevented. Later we'll see what it means for custom events.
|
||||||
|
|
||||||
By default both flags are false: `{bubbles: false, cancelable: false}`.
|
By default both are false: `{bubbles: false, cancelable: false}`.
|
||||||
|
|
||||||
## The method dispatchEvent
|
## dispatchEvent
|
||||||
|
|
||||||
After an event object is created, we should "run" it on an element using the call `elem.dispatchEvent(event)`.
|
After an event object is created, we should "run" it on an element using the call `elem.dispatchEvent(event)`.
|
||||||
|
|
||||||
|
@ -73,8 +73,8 @@ All we need is to set `bubbles` to `true`:
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
1. We must use `addEventListener` for our custom events, because `on<event>` only exists for built-in events.
|
1. We should use `addEventListener` for our custom events, because `on<event>` only exists for built-in events, `document.onhello` doesn't work.
|
||||||
2. Must set `bubbles`, otherwise the event won't bubble up.
|
2. Must set `bubbles:true`, otherwise the event won't bubble up.
|
||||||
|
|
||||||
The bubbling mechanics is the same for built-in (`click`) and custom (`hello`) events. There are also capturing and bubbling stages.
|
The bubbling mechanics is the same for built-in (`click`) and custom (`hello`) events. There are also capturing and bubbling stages.
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ In the second argument (object) we can add an additional property `detail` for a
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
```html run
|
```html run refresh
|
||||||
<h1 id="elem">Hello for John!</h1>
|
<h1 id="elem">Hello for John!</h1>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -155,29 +155,29 @@ For instance:
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `detail` can be anything. Once again, technically we can assign any properties into a regular `Event` object after its creation. But `CustomEvent` clearly states that the event is not built-in, but our own. And reserves the special `detail` field, so that we can write to it safely, without conflicts with standard event properties.
|
The `detail` property can have any data.
|
||||||
|
|
||||||
|
Once again, technically we can assign any properties into a regular `new Event` object after its creation. But `CustomEvent` provides the special `detail` field, so that we can write to it safely, without conflicts with standard event properties.
|
||||||
|
|
||||||
|
Also using the right class tells something about "what kind of event" it is, and `CustomEvent` is better for that matter. So we should use it for our events.
|
||||||
|
|
||||||
|
## event.preventDefault()
|
||||||
|
|
||||||
|
We can call `event.preventDefault()` on a script-generated event if `cancelable:true` flag is specified.
|
||||||
|
|
||||||
|
Of course, if the event has a non-standard name, then it's not known to the browser, and there's no "default browser action" for it.
|
||||||
|
|
||||||
|
But the event-generating code may plan some actions after `dispatchEvent`.
|
||||||
|
|
||||||
## Отмена действия по умолчанию
|
The call of `event.preventDefault()` is a way for the handler to send a signal that those actions shouldn't be performed.
|
||||||
|
|
||||||
На сгенерированном событии, как и на встроенном браузерном, обработчик может вызвать метод `event.preventDefault()`. Тогда `dispatchEvent` возвратит `false`.
|
In that case the call to `elem.dispatchEvent(event)` returns `false`. And the event-generating code knows that the processing shouldn't continue.
|
||||||
|
|
||||||
Остановимся здесь подробнее. Обычно `preventDefault()` вызов предотвращает действие браузера. В случае, если событие придумано нами, имеет нестандартное имя -- никакого действия браузера, конечно, нет.
|
For instance, in the example below there's a `hide()` function. It generates the `"hide"` event on the element `#rabbit`, notifying all interested parties that the rabbit is going to hide.
|
||||||
|
|
||||||
Но код, который генерирует событие, может предусматривать какие-то ещё действия после `dispatchEvent`.
|
A handler set by `rabbit.addEventListener('hide',...)` will learn about that and, if it wants, can prevent that action by calling `event.preventDefault()`. Then the rabbit won't hide:
|
||||||
|
|
||||||
Вызов `event.preventDefault()` является возможностью для обработчика события сообщить в сгенерировавший событие код, что эти действия продолжать не надо.
|
```html run refresh
|
||||||
|
|
||||||
В примере ниже есть функция `hide()`, которая при вызове генерирует событие `hide` на элементе `#rabbit`, уведомляя всех интересующихся, что кролик собирается спрятаться.
|
|
||||||
|
|
||||||
Любой обработчик может узнать об этом, подписавшись на событие через `rabbit.addEventListener('hide',...)` и, при желании, отменить действие по умолчанию через `event.preventDefault()`. Тогда кролик не исчезнет:
|
|
||||||
|
|
||||||
```html run
|
|
||||||
<pre id="rabbit">
|
<pre id="rabbit">
|
||||||
|\ /|
|
|\ /|
|
||||||
\|_|/
|
\|_|/
|
||||||
|
@ -187,46 +187,49 @@ The `detail` can be anything. Once again, technically we can assign any properti
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// hide() will be called automatically in 2 seconds
|
||||||
function hide() {
|
function hide() {
|
||||||
var event = new Event("hide", {
|
let event = new Event("hide", {
|
||||||
cancelable: true
|
cancelable: true // without that flag preventDefault doesn't work
|
||||||
});
|
});
|
||||||
if (!rabbit.dispatchEvent(event)) {
|
if (!rabbit.dispatchEvent(event)) {
|
||||||
alert( 'действие отменено обработчиком' );
|
alert('the action was prevented by a handler');
|
||||||
} else {
|
} else {
|
||||||
rabbit.hidden = true;
|
rabbit.hidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rabbit.addEventListener('hide', function(event) {
|
rabbit.addEventListener('hide', function(event) {
|
||||||
if (confirm("Вызвать preventDefault?")) {
|
if (confirm("Call preventDefault?")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// прячемся через 2 секунды
|
// hide in 2 seconds
|
||||||
setTimeout(hide, 2000);
|
setTimeout(hide, 2000);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
To generate an event, we first need to create an event object.
|
||||||
|
|
||||||
|
The generic `Event(name, options)` constructor accepts an arbitrary event name and the `options` object with two properties:
|
||||||
|
- `bubbles:true` if the event should bubble.
|
||||||
|
- `cancelable:true` is the `event.preventDefault()` should work.
|
||||||
|
|
||||||
|
Other constructors of native events like `MouseEvent`, `KeyboardEvent` and so on accept properties specific to that event type. For instance, `clientX` for mouse events.
|
||||||
|
|
||||||
## Итого
|
For custom events we should use `CustomEvent` constructor. It has an additional option named `detail`, we should assign the event-specific data to it. Then all handlers can access it as `event.detail`.
|
||||||
|
|
||||||
- Все браузеры, кроме IE9-11, позволяют генерировать любые события, следуя стандарту DOM4.
|
Despite the technical possibility to generate browser events like `click` or `keydown`, we should use with the great care.
|
||||||
- В IE9+ поддерживается более старый стандарт, можно легко сделать полифилл, например для `CustomEvent` он рассмотрен в этой главе.
|
|
||||||
- IE8- может генерировать только встроенные события.
|
|
||||||
|
|
||||||
Несмотря на техническую возможность генерировать встроенные браузерные события типа `click` или `keydown` -- пользоваться ей стоит с большой осторожностью.
|
We shouldn't generate browser events as a hacky way to run handlers. That's a bad architecture most of the time.
|
||||||
|
|
||||||
В 98% случаев, когда разработчик начинающего или среднего уровня хочет сгенерировать *встроенное* событие -- это вызвано "кривой" архитектурой кода, и взаимодействие нужно на уровне выше.
|
Native events might be generated:
|
||||||
|
|
||||||
Как правило события имеет смысл генерировать:
|
- As a dirty hack to make 3rd-party libraries work the needed way, if they don't provide other means of interaction.
|
||||||
|
- For automated testing, to "click the button" in the script and see if the interface reacts correctly.
|
||||||
|
|
||||||
- Либо как явный и грубый хак, чтобы заставить работать сторонние библиотеки, в которых не предусмотрены другие средства взаимодействия.
|
Custom events with our own names are often generated for architectural purposes, to signal what happens inside our menus, sliders, carousels etc.
|
||||||
- Либо для автоматического тестирования, чтобы скриптом "нажать на кнопку" и посмотреть, произошло ли нужное действие.
|
|
||||||
- Либо при создании своих "элементов интерфейса". Например, никто не мешает при помощи JavaScript создать из `<div class="calendar">` красивый календарь и генерировать на нём событие `change` при выборе даты. Эту тему мы разовьём позже.
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue