diff --git a/2-ui/2-events/index.md b/2-ui/2-events/index.md index ffec0857..07f06fdd 100644 --- a/2-ui/2-events/index.md +++ b/2-ui/2-events/index.md @@ -1,3 +1,3 @@ -# Events +# Introduction into Events An introduction to browser events, event properties and handling patterns. diff --git a/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/solution.md b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/solution.md new file mode 100644 index 00000000..e69de29b diff --git a/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/solution.view/index.html b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/solution.view/index.html new file mode 100755 index 00000000..56756633 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/solution.view/index.html @@ -0,0 +1,62 @@ + + + + + + + + + + + Click on a list item to select it. +
+ + + + + + + diff --git a/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/source.view/index.html b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/source.view/index.html new file mode 100755 index 00000000..e18d4a99 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/source.view/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + Click on a list item to select it. +
+ + + + + + + diff --git a/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/task.md b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/task.md new file mode 100644 index 00000000..f358616e --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/01-selectable-list/task.md @@ -0,0 +1,17 @@ +importance: 5 + +--- + +# Selectable list + +Create a list where elements are selectable, like in file-managers. + +- A click on a list element selects only that element (adds the class `.selected`), deselects all others. +- If a click is made with `key:Ctrl` (`key:Cmd` for Mac), then the selection is toggled on the element, but other elements are not modified. + +The demo: + +[iframe border="1" src="solution" height=180] + +P.S. For this task we can assume that list items are text-only. No nested tags. +P.P.S. Prevent the native browser selection of the text on clicks. diff --git a/2-ui/3-event-details/1-mouse-clicks/article.md b/2-ui/3-event-details/1-mouse-clicks/article.md new file mode 100644 index 00000000..c5d80ad6 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/article.md @@ -0,0 +1,260 @@ +# Mouse: clicks, coordinates + +In this chapter we'll get into more details of mouse events and their properties. + +[cut] + +## Mouse event types + +We can split mouse events into two categories: "simple" and "complex" + +### Simple events + +The most used simple events are: + +`mousedown/mouseup` +: Mouse button is clicked/released over an element. + +`mouseover/mouseout` +: Mouse pointer comes over/out an element. + +`mousemove` +: Every mouse move over an element triggers that event. + +There are several other event types too, we'll cover them later. + +### Complex events + +`click` +: Triggers after `mousedown` and then `mouseup` over the same element. + +`contextmenu` +: Triggers after `mousedown` if the right mouse button was used. + +`dblclick` +: Triggers after a double click over an element. + +Complex events are made of simple ones, so in theory we could live without them. But they exist, and that's good, because they are convenient. + +For touchscreen devices mouse events also happen, because they are emulated. + +### Events order + +An action may trigger multiple events. + +For instance, a click first triggers `mousedown`, when the button is pressed, then `mouseup` and `click` when it's released. + +In cases when a single action initiated multiple events, their order is fixed. That is, the handlers are be called in the order `mousedown` -> `mouseup` -> `click`. Events are handled in sequence: `onmouseup` finishes before `onclick` runs. + +```online +Click the button below and you'll see which events happen. Try double-click too. + +On the teststand below all mouse events are logged, and if there are more than 1 second delay between them, then they are separated by a horizontal ruler. + +Also we can see the `which` property that allows to detect the mouse button. We'll cover it a bit later. + +
+``` + +## Getting the button: which + +Click-related events always have the `which` property that allows to see the button. + +It is not used for `click` and `contextmenu` events, because the former happens only on left-click, and the latter -- only on right-click. + +But if we track `mousedown` and `mouseup`, then we need it, because they trigger on any button. + +There are the three possible values: + +- `event.which == 1` -- the left button +- `event.which == 2` - the middle button +- `event.which == 3` - the right button + +The middle button is somewhat exotic right now. + +## Modifiers: shift, alt, ctrl and meta + +All mouse events include the information about pressed modifier keys. + +The properties are: + +- `shiftKey` +- `altKey` +- `ctrlKey` +- `metaKey` (for Mac) + +For instance, the button below only works on `key:Alt+Shift`+click: + +```html autorun height=60 + + + +``` + +```warn header="Attention: on Mac it's usually `Cmd` instead of `Ctrl`" +On Windows and Linux there are modifier keys `key:Alt`, `key:Shift` and `key:Ctrl`. On Mac there's one more: `key:Cmd`, that corresponds to the property `metaKey`. + +In most cases when Windows/Linux uses `key:Ctrl`, on Mac people use `key:Cmd`. So where a Windows user presses `key:Ctrl+Enter` or `key:Ctrl+A`, a Mac user would press `key:Cmd+Enter` or `key:Cmd+A`, and so on, most apps use `key:Cmd` instead of `key:Ctrl`. + +So if we want to support combinations like `key:Ctrl`+click, then for Mac it makes sense to use `key:Cmd`+click. That's more comfortable for Mac users. + +Even if we'd like to force Mac users to `key:Ctrl`+click -- that's kind of difficult. The problem is: a regular click with `key:Ctrl` is interpreted as a *right click* on Mac, and it generates the `contextmenu` event, not `click` like Windows/Linux. + +So if we want users of all operational systems to feel comfortable, then together with `ctrlKey` we should use `metaKey`. + +For JS-code it means that we should check `if (event.ctrlKey || event.metaKey)`. +``` + +```warn header="There are also mobile devices" +Keyboard combinations are good as an addition to the workflow. So that if you have keyboard -- it works. And if your device doesn't have it -- then there's another way to do the same. +``` + +## Coordinates: clientX/Y, pageX/Y + +All mouse events have coordinates in two flavours: + +1. Window-relative: `clientX` and `clientY`. +2. Document-relative: `pageX` and `pageY`. + +See more about coordinates the chapter . + +For instance, if we have a window of the size 500x500, and the mouse is in the center, then `clientX` and `clientY` are `250`. + +If we scroll the page, but the mouse is still in the center, then `clientX/Y` don't change, because they are window-relative. + +````online +Move the mouse over the input field to see `clientX/clientY`: + +```html autorun height=50 + +``` +```` + +That's like `elem.getBoundingClientRect()` and `position:fixed`. + +Document-relative coordinates are counted from the left-upper corner of the document, not the window. In case of a scrolled page, they also include the scrolled out left-upper part. + +These coordinates are connected by the formulas: + +```js +// for an arbitrary mouse event +event.pageX = pageXOffset + event.clientX +event.pageY = pageYOffset + event.clientY +``` + +So technically we don't need `pageX/Y`, because we can always calculate them using the formulas. But it's good that we have them, as a matter of convenience. + +## No selection on mousedown + +Mouse clicks have a side-effect that may be disturbing. A double click or an occasional cursor move with a pressed button select the text. + +If we want to handle click events ourselves, then the "extra" selection doesn't look good. + +For instance, a double-click on the text below selects it in addition to our handler: + +```html autorun height=50 +Double-click me +``` + +There's a CSS way to stop the selection: the `user-select` property from [CSS UI Draft](https://www.w3.org/TR/css-ui-4/). + +It's yet in the draft, so browser support it with prefixes: + +```html autorun height=50 + + +Before... + + Unselectable + +...After +``` + +Now if you double-click on `"Unselectable"`, it doesn't get selected. Seems to work. + +...But there is a side-effect! The text became truly unselectable. Even if a user starts the selection from `"Before"` and ends with `"After"`, the selection skips `"Unselectable"` part. Do we really want to make our text unselectable? + +Most of time, not really. A user may want to select it, for copying or other needs. That may be disturbing if we don't allow him to do it. So the solution is not that good. + +What we want is to "fix" our interface. We don't want the selection to occur on double-click, that's it. + +An alternative solution would be to handle `mousedown`, like this: + +```html autorun height=50 +Before... + + Double-click me + +...After +``` + +The selection is started on `mousedown` as a default browser action. So if we prevent it, then the bold element is not selected any more on clicks. That's as intended. + +From the other hand, the text inside it is still selectable. The only limitation: the selection should start not on the text itself, but from "before" or "after" it. Usually that's not a problem. + +````smart header="Canceling the selection" +Instead of *preventing* the selection, we can cancel it "post-factum" in the event handler. + +Here's how: + +```html autorun height=50 +Before... + + Double-click me + +...After +``` + +If you double-click on the bold element, then the selection appears and then is immediately removed. That doesn't look nice, and is not fully reliable though. +```` + +````smart header="Preventing copying" +If we want to disable selection to protect our content from copy-pasting, then we can use another event: `oncopy`. + +```html autorun height=80 no-beautify +
+ Dear user, + The copying is forbidden for you. + If you know JS or HTML, then that's not a problem of course, + otherwise we're sorry. +
+``` +If you try to copy a piece of text in the `
`, that won't work, because the default action `oncopy` is prevented. + +Surely that doesn't stop from opening HTML-source and doing things manually, but not everyone knows how to do it. +```` + +## Summary + +Mouse events have following properties: + +- Button: `which` +- Modifier keys (`true` if pressed): `altKey`, `ctrlKey`, `shiftKey` and `metaKey` (Mac). + - If you want to handle `key:Ctrl`, then don't forget Mac users, they use `key:Cmd`, so it's better to check `if (e.metaKey || e.ctrlKey)`. + +- Window-relative coordinates: `clientX/clientY` +- Document-relative coordinates: `pageX/clientX` + +In the tasks below it's also important to deal with the selection as an unwanted side-effect of clicks. + +There are several ways, for instance: +1. CSS-property `user-select:none` (with browser prefixes) completely disables it. +2. Cancel the selection post-factum using `getSelection().removeAllRanges()`. +3. Handle `mousedown` and prevent the default action. + +The third way is preferred most of the time. diff --git a/2-ui/3-event-details/1-mouse-clicks/head.html b/2-ui/3-event-details/1-mouse-clicks/head.html new file mode 100644 index 00000000..461f0e85 --- /dev/null +++ b/2-ui/3-event-details/1-mouse-clicks/head.html @@ -0,0 +1,47 @@ + diff --git a/2-ui/3-event-details/10-onload-ondomcontentloaded/article.md b/2-ui/3-event-details/10-onload-ondomcontentloaded/article.md new file mode 100644 index 00000000..bb5f4e17 --- /dev/null +++ b/2-ui/3-event-details/10-onload-ondomcontentloaded/article.md @@ -0,0 +1,204 @@ +# Загрузка документа: DOMContentLoaded, load, beforeunload, unload + +Процесс загрузки HTML-документа, условно, состоит из трёх стадий: + +- `DOMContentLoaded` -- браузер полностью загрузил HTML и построил DOM-дерево. +- `load` -- браузер загрузил все ресурсы. +- `beforeunload/unload` -- уход со страницы. + +Все эти стадии очень важны. На каждую можно повесить обработчик, чтобы совершить полезные действия: + +- `DOMContentLoaded` -- означает, что все DOM-элементы разметки уже созданы, можно их искать, вешать обработчики, создавать интерфейс, но при этом, возможно, ещё не догрузились какие-то картинки или стили. +- `load` -- страница и все ресурсы загружены, используется редко, обычно нет нужды ждать этого момента. +- `beforeunload/unload` -- можно проверить, сохранил ли посетитель изменения, уточнить, действительно ли он хочет покинуть страницу. + +Далее мы рассмотрим важные детали этих событий. + +[cut] + +## DOMContentLoaded + +Событие `DOMContentLoaded` происходит на `document` и поддерживается во всех браузерах, кроме IE8-. Про поддержку аналогичного функционала в старых IE мы поговорим в конце главы. + +Обработчик на него вешается только через `addEventListener`: + +```js +document.addEventListener("DOMContentLoaded", ready); +``` + +Пример: + +```html run height=150 + + + +``` + +В примере выше обработчик `DOMContentLoaded` сработает сразу после загрузки документа, не дожидаясь получения картинки. + +Поэтому на момент вывода `alert` и сама картинка будет невидна и её размеры -- неизвестны (кроме случая, когда картинка взята из кеша браузера). + +В своей сути, событие `onDOMContentLoaded` -- простое, как пробка. Полностью создано DOM-дерево -- и вот событие. Но с ним связан ряд существенных тонкостей. + +### DOMContentLoaded и скрипты + +Если в документе есть теги ` +``` + +Такое поведение прописано в стандарте. Его причина -- скрипт может захотеть получить информацию со страницы, зависящую от стилей, например, ширину элемента, и поэтому обязан дождаться загрузки `style.css`. + +**Побочный эффект -- так как событие `DOMContentLoaded` будет ждать выполнения скрипта, то оно подождёт и загрузки стилей, которые идут перед ` + + +``` + +## window.onunload + +Когда человек уходит со страницы или закрывает окно, на `window` срабатывает событие `unload`. В нём можно сделать что-то, не требующее ожидания, например, закрыть вспомогательные popup-окна, но отменить сам переход нельзя. + +Это позволяет другое событие -- `onbeforeunload`, которое поэтому используется гораздо чаще. + +## window.onbeforeunload [#window.onbeforeunload] + +Если посетитель инициировал переход на другую страницу или нажал "закрыть окно", то обработчик `onbeforeunload` может приостановить процесс и спросить подтверждение. + +Для этого ему нужно вернуть строку, которую браузеры покажут посетителю, спрашивая -- нужно ли переходить. + +Например: + +```js +window.onbeforeunload = function() { + return "Данные не сохранены. Точно перейти?"; +}; +``` + +```warn header="Firefox игнорирует текст, он показывает своё сообщение" +Firefox игнорирует текст, а всегда показывает своё сообщение. Это сделано в целях большей безопасности посетителя, чтобы его нельзя было ввести в заблуждение сообщением. +``` + +```online +Кликните на кнопку в `IFRAME'е` ниже, чтобы поставить обработчик, а затем по ссылке, чтобы увидеть его в действии: + +[iframe src="window-onbeforeunload" border="1" height="80" link] +``` + +## Эмуляция DOMContentLoaded для IE8- + +Прежде чем что-то эмулировать, заметим, что альтернативой событию `onDOMContentLoaded` является вызов функции `init` из скрипта в самом конце `BODY`, когда основная часть DOM уже готова: + +```html + + ... + + +``` + +Причина, по которой обычно предпочитают именно событие -- одна: удобство. Вешается обработчик и не надо ничего писать в конец `BODY`. + +### Мини-скрипт documentReady +Если вы всё же хотите использовать `onDOMContentLoaded` кросс-браузерно, то нужно либо подключить какой-нибудь фреймворк -- почти все предоставляют такой функционал, либо использовать функцию из мини-библиотеки [jquery.documentReady.js](https://github.com/Couto/jquery.parts/blob/master/jquery.documentReady.js). + +Несмотря на то, что в названии содержится слово "jquery", эта библиотечка не требует [jQuery](http://jquery.com). Наоборот, она представляет собой единственную функцию с названием `$`, вызов которой `$(callback)` добавляет обработчик `callback` на `DOMContentLoaded` (можно вызывать много раз), либо, если документ уже загружен -- выполняет его тут же. + +Пример использования: + +```html run + + + + + +
Текст страницы
+``` + +Здесь `alert` сработает до загрузки картинки, но после создания DOM, в частности, после появления текста. И так будет для всех браузеров, включая даже очень старые IE. + +````smart header="Как именно эмулируется `DOMContentLoaded`?" +Технически, эмуляция `DOMContentLoaded` для старых IE осуществляется очень забавно. + +Основной приём -- это попытка прокрутить документ вызовом: + +```js +document.documentElement.doScroll("left"); +``` + +Метод `doScroll` работает только в IE и "методом тыка" было обнаружено, что он бросает исключение, если DOM не полностью создан. + +Поэтому библиотека пытается вызвать прокрутку, если не получается -- через `setTimeout(.., 1)` пытается прокрутить его ещё раз, и так до тех пор, пока действие не перестанет вызывать ошибку. На этом этапе документ считается загрузившимся. + +Внутри фреймов и в очень старых браузерах такой подход может ошибаться, поэтому дополнительно ставится резервный обработчик на `onload`, чтобы уж точно сработал. +```` + +## Итого + +- Самое востребованное событие из описанных -- без сомнения, `DOMContentLoaded`. Многие страницы сделаны так, что инициализуют интерфейсы именно по этому событию. + + Это удобно, ведь можно в `` написать скрипт, который будет запущен в момент, когда все DOM-элементы доступны. + + С другой стороны, следует иметь в виду, что событие `DOMContentLoaded` будет ждать не только, собственно, HTML-страницу, но и внешние скрипты, подключенные тегом ` + + + + Уйти на EXAMPLE.COM + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.md b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.md new file mode 100644 index 00000000..7e90a02a --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.md @@ -0,0 +1,8 @@ +# Подсказка + +Текст на странице пусть будет изначально `DIV`, с классом `img-replace` и атрибутом `data-src` для картинки. + +Функция `replaceImg()` должна искать такие `DIV` и загружать изображение с указанным `src`. По `onload` осуществляется замена `DIV` на картинку. + +# Решение + diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html new file mode 100755 index 00000000..4935f238 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/solution.view/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + +
+
+ Google +
+ +
+ Яндекс +
+ +
bing
+ + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html new file mode 100755 index 00000000..eba12fd0 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/source.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + +
+ Google +
+ + +
+ Яндекс +
+ + +
bing
+ +
+ + + Яндекс + Google + Файла нет (bing) + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md new file mode 100644 index 00000000..25fc4f02 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/1-nice-alt/task.md @@ -0,0 +1,20 @@ +importance: 5 + +--- + +# Красивый "ALT" + +Обычно, до того как изображение загрузится (или при отключенных картинках), посетитель видит пустое место с текстом из "ALT". Но этот атрибут не допускает HTML-форматирования. + +При мобильном доступе скорость небольшая, и хочется, чтобы посетитель сразу видел красивый текст. + +**Реализуйте "красивый" (HTML) аналог `alt` при помощи CSS/JavaScript, который затем будет заменён картинкой сразу же как только она загрузится.** А если загрузка не состоится -- то не заменён. + +Демо: (нажмите "перезагрузить", чтобы увидеть процесс загрузки и замены) + +[iframe src="solution" height="100"] + +Картинки для `bing` специально нет, так что текст остается "как есть". + +Исходный документ содержит разметку текста и ссылки на изображения. + diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.md b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.md new file mode 100644 index 00000000..e7fb799a --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.md @@ -0,0 +1,5 @@ + +Создайте переменную-счетчик для подсчёта количества загруженных картинок, и увеличивайте при каждом `onload/onerror`. + +Когда счетчик станет равен количеству картинок -- вызывайте `callback`. + diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.view/index.html new file mode 100755 index 00000000..23825080 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/solution.view/index.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/source.view/index.html new file mode 100755 index 00000000..1e3afe5a --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/source.view/index.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/task.md b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/task.md new file mode 100644 index 00000000..edf99d23 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/2-load-img-callback/task.md @@ -0,0 +1,20 @@ +importance: 4 + +--- + +# Загрузить изображения с коллбэком + +Создайте функцию `preloadImages(sources, callback)`, которая предзагружает изображения из массива `sources`, и после загрузки вызывает функцию `callback`. + +Пример использования: + +```js +preloadImages(["1.jpg", "2.jpg", "3.jpg"], callback); +``` + +Если вдруг возникает ошибка при загрузке -- считаем такое изображение загруженным, чтобы не ломать поток выполнения. + +Такая функция может полезна, например, для фоновой загрузки картинок в онлайн-галерею. + +В исходном документе содержатся ссылки на картинки, а также код для проверки, действительно ли изображения загрузились. Он должен выводить "0", затем "300". + diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.md b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.md new file mode 100644 index 00000000..753fe747 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.md @@ -0,0 +1,18 @@ +# Подсказка + +Добавляйте `SCRIPT` при помощи методов `DOM`: + +```js +var script = document.createElement('script'); +script.src = src; + +// в документе может не быть HEAD или BODY, +// но хотя бы один (текущий) SCRIPT в документе есть +var s = document.getElementsByTagName('script')[0]; +s.parentNode.insertBefore(script, s); // перед ним и вставим +``` + +На скрипт повесьте обработчики `onload/onreadystatechange`. + +# Решение + diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/go.js b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/go.js new file mode 100755 index 00000000..1729e58f --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/go.js @@ -0,0 +1,3 @@ +function go() { + alert("ok"); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/index.html new file mode 100755 index 00000000..e840c12d --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/solution.view/index.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/go.js b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/go.js new file mode 100755 index 00000000..1729e58f --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/go.js @@ -0,0 +1,3 @@ +function go() { + alert("ok"); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/index.html new file mode 100755 index 00000000..fb498558 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/source.view/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/3-script-callback/task.md b/2-ui/3-event-details/11-onload-onerror/3-script-callback/task.md new file mode 100644 index 00000000..2c5d5751 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/3-script-callback/task.md @@ -0,0 +1,21 @@ +importance: 4 + +--- + +# Скрипт с коллбэком + +Создайте функцию `addScript(src, callback)`, которая загружает скрипт с данным `src`, и после его загрузки и выполнения вызывает функцию `callback`. + +Скрипт может быть любым, работа функции не должна зависеть от его содержимого. + +Пример использования: + +```js +// go.js содержит функцию go() +addScript("go.js", function() { + go(); +}); +``` + +Ошибки загрузки обрабатывать не нужно. + diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.md b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.md new file mode 100644 index 00000000..cfe4d03f --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.md @@ -0,0 +1,8 @@ +# Подсказки + +Создайте переменную-счетчик для подсчёта количества загруженных скриптов. + +Чтобы один скрипт не учитывался два раза (например, `onreadystatechange` запустился при `loaded` и `complete`), учитывайте его состояние в объекте `loaded`. Свойство `loaded[i] = true` означает что `i`-й скрипт уже учтён. + +# Решение + diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/a.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/a.js new file mode 100755 index 00000000..9d6742cd --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/a.js @@ -0,0 +1,3 @@ +function a() { + b(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/b.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/b.js new file mode 100755 index 00000000..ce1689f2 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/b.js @@ -0,0 +1,3 @@ +function b() { + c(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/c.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/c.js new file mode 100755 index 00000000..e343ee11 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/c.js @@ -0,0 +1,3 @@ +function c() { + alert('ok'); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/index.html b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/index.html new file mode 100755 index 00000000..a98093ad --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/solution.view/index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/a.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/a.js new file mode 100755 index 00000000..9d6742cd --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/a.js @@ -0,0 +1,3 @@ +function a() { + b(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/b.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/b.js new file mode 100755 index 00000000..ce1689f2 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/b.js @@ -0,0 +1,3 @@ +function b() { + c(); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/c.js b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/c.js new file mode 100755 index 00000000..e343ee11 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/c.js @@ -0,0 +1,3 @@ +function c() { + alert('ok'); +} \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/index.html b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/index.html new file mode 100755 index 00000000..cff5595f --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/source.view/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/task.md b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/task.md new file mode 100644 index 00000000..da8ac3f6 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/4-scripts-callback/task.md @@ -0,0 +1,22 @@ +importance: 5 + +--- + +# Скрипты с коллбэком + +Создайте функцию `addScripts(scripts, callback)`, которая загружает скрипты из массива `scripts`, и *после загрузки и выполнения их всех* вызывает функцию `callback`. + +Скрипт может быть любым, работа функции не должна зависеть от его содержимого. + +Пример использования: + +```js no-beautify +addScripts(["a.js", "b.js", "c.js"], function() { a() }); +/* функция a() описана в a.js и использует b.js,c.js */ +``` + +- Ошибки загрузки обрабатывать не нужно. +- Один скрипт не ждёт другого. Они все загружаются, а по окончании вызывается обработчик `callback`. + +Исходный код содержит скрипты `a.js`, `b.js`, `c.js`: + diff --git a/2-ui/3-event-details/11-onload-onerror/article.md b/2-ui/3-event-details/11-onload-onerror/article.md new file mode 100644 index 00000000..253fb826 --- /dev/null +++ b/2-ui/3-event-details/11-onload-onerror/article.md @@ -0,0 +1,228 @@ +# Загрузка скриптов, картинок, фреймов: onload и onerror + +Браузер позволяет отслеживать загрузку внешних ресурсов -- скриптов, ифреймов, картинок и других. + +Для этого есть два события: + +- `onload` -- если загрузка успешна. +- `onerror` -- если при загрузке произошла ошибка. + +## Загрузка SCRIPT + +Рассмотрим следующую задачу. + +В браузере работает сложный интерфейс и, чтобы создать очередной компонент, нужно загрузить скрипт с сервера. + +Подгрузить внешний скрипт -- достаточно просто: + +```js +var script = document.createElement('script'); +script.src = "my.js"; + +document.body.appendChild(script); +``` + +...Но как после подгрузки выполнить функцию, которая объявлена в этом скрипте? Для этого нужно отловить момент окончания загрузки и выполнения тега ` + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/source.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/source.view/index.html new file mode 100644 index 00000000..8abb923d --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/source.view/index.html @@ -0,0 +1,70 @@ + + + + + + + + + + + +
+
+ +

Once upon a time there was a mother pig who had three little pigs.

+ +

The three little pigs grew so big that their mother said to them, "You are too big to live here any longer. You must go and build houses for yourselves. But take care that the wolf does not catch you." + +

The three little pigs set off. "We will take care that the wolf does not catch us," they said.

+ +

Soon they met a man. Hover over me

+ +
+ + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md new file mode 100644 index 00000000..435bac0f --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md @@ -0,0 +1,25 @@ +importance: 5 + +--- + +# Improved tooltip behavior + +Write JavaScript that shows a tooltip over an element with the attribute `data-tooltip`. + +That's like the task , but here the annotated elements can be nested. The most deeply nested tooltip is shown. + +For instance: + +```html +
+
+ ... + Hover over me +
+``` + +The result in iframe: + +[iframe src="solution" height=300 border=1] + +P.S. Hint: only one tooltip may show up at the same time. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md new file mode 100644 index 00000000..c4af78b1 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md @@ -0,0 +1,18 @@ + +The algorithm looks simple: +1. Put `onmouseover/out` handlers on the element. Also can use `onmouseenter/leave` here, but they are less universal, won't work if we introduce delegation. +2. When a mouse cursor entered the element, start measuring the speed on `mousemove`. +3. If the speed is slow, then run `over`. +4. Later if we're out of the element, and `over` was executed, run `out`. + +The question is: "How to measure the speed?" + +The first idea would be: to run our function every `100ms` and measure the distance between previous and new coordinates. If it's small, then the speed is small. + +Unfortunately, there's no way to get "current mouse coordinates" in JavaScript. There's no function like `getCurrentMouseCoordinates()`. + +The only way to get coordinates is to listen to mouse events, like `mousemove`. + +So we can set a handler on `mousemove` to track coordinates and remember them. Then we can compare them, once per `100ms`. + +P.S. Please note: the solution tests use `dispatchEvent` to see if the tooltip works right. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/hoverIntent.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/hoverIntent.js new file mode 100644 index 00000000..ca1ac04c --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/hoverIntent.js @@ -0,0 +1,106 @@ +'use strict'; + +class HoverIntent { + + constructor({ + sensitivity = 0.1, // speed less than 0.1px/ms means "hovering over an element" + interval = 100, // measure mouse speed once per 100ms + elem, + over, + out + }) { + this.sensitivity = sensitivity; + this.interval = interval; + this.elem = elem; + this.over = over; + this.out = out; + + // make sure "this" is the object in event handlers. + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseOver = this.onMouseOver.bind(this); + this.onMouseOut = this.onMouseOut.bind(this); + + // and in time-measuring function (called from setInterval) + this.trackSpeed = this.trackSpeed.bind(this); + + elem.addEventListener("mouseover", this.onMouseOver); + + elem.addEventListener("mouseout", this.onMouseOut); + + } + + onMouseOver(event) { + + if (this.isOverElement) { + // if we're over the element, then ignore the event + // we are already measuring the speed + return; + } + + this.isOverElement = true; + + // after every mousemove we'll be check the distance + // between the previous and the current mouse coordinates + // if it's less than sensivity, then the speed is slow + + this.prevX = event.pageX; + this.prevY = event.pageY; + this.prevTime = Date.now(); + + elem.addEventListener('mousemove', this.onMouseMove); + this.checkSpeedInterval = setInterval(this.trackSpeed, this.interval); + } + + onMouseOut(event) { + // if left the element + if (!event.relatedTarget || !elem.contains(event.relatedTarget)) { + this.isOverElement = false; + this.elem.removeEventListener('mousemove', this.onMouseMove); + clearInterval(this.checkSpeedInterval); + if (this.isHover) { + // если была остановка над элементом + this.out.call(this.elem, event); + this.isHover = false; + } + } + } + + onMouseMove(event) { + this.lastX = event.pageX; + this.lastY = event.pageY; + this.lastTime = Date.now(); + } + + trackSpeed() { + + let speed; + + if (!this.lastTime || this.lastTime == this.prevTime) { + // cursor didn't move + speed = 0; + } else { + speed = Math.sqrt( + Math.pow(this.prevX - this.lastX, 2) + + Math.pow(this.prevY - this.lastY, 2) + ) / (this.lastTime - this.prevTime); + } + + if (speed < this.sensitivity) { + clearInterval(this.checkSpeedInterval); + this.isHover = true; + this.over.call(this.elem, event); + } else { + // speed fast, remember new coordinates as the previous ones + this.prevX = this.lastX; + this.prevY = this.lastY; + this.prevTime = this.lastTime; + } + } + + destroy() { + elem.removeEventListener('mousemove', this.onMouseMove); + elem.removeEventListener('mouseover', this.onMouseOver); + elem.removeEventListener('mouseout', this.onMouseOut); + }; + +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/index.html new file mode 100644 index 00000000..df0f1365 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/index.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + +
+ 12 : + 30 : + 00 +
+ + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/style.css new file mode 100644 index 00000000..2fcd035e --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/style.css @@ -0,0 +1,38 @@ +.hours { + color: red; +} + +body#mocha { + margin: 0; +} + +.minutes { + color: green; +} + +.seconds { + color: blue; +} + +.clock { + border: 1px dashed black; + padding: 5px; + display: inline-block; + background: yellow; + position: absolute; + left: 0; + top: 0; +} + +#tooltip { + position: absolute; + padding: 10px 20px; + border: 1px solid #b3c9ce; + border-radius: 4px; + text-align: center; + font: italic 14px/1.3 arial, sans-serif; + color: #333; + background: #fff; + z-index: 100000; + box-shadow: 3px 3px 3px rgba(0, 0, 0, .3); +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/test.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/test.js new file mode 100644 index 00000000..7c7f6d23 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/test.js @@ -0,0 +1,98 @@ +'use strict'; + +describe("hoverIntent", function() { + + function mouse(eventType, x, y, options) { + let eventOptions = Object.assign({ + bubbles: true, + clientX: x, + clientY: y, + pageX: x, + pageY: y, + target: elem + }, options || {}); + + elem.dispatchEvent(new MouseEvent(eventType, eventOptions)); + } + + + let isOver; + let hoverIntent; + + + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + this.clock.restore(); + }); + + + beforeEach(function() { + isOver = false; + + hoverIntent = new HoverIntent({ + elem: elem, + over: function() { + isOver = true; + }, + out: function() { + isOver = false; + } + }); + }) + + afterEach(function() { + if (hoverIntent) { + hoverIntent.destroy(); + } + }) + + it("mouseover -> immediately no tooltip", function() { + mouse('mouseover', 10, 10); + assert.isFalse(isOver); + }); + + it("mouseover -> pause shows tooltip", function() { + mouse('mouseover', 10, 10); + this.clock.tick(100); + assert.isTrue(isOver); + }); + + it("mouseover -> fast mouseout no tooltip", function() { + mouse('mouseover', 10, 10); + setTimeout( + () => mouse('mouseout', 300, 300, { relatedTarget: document.body}), + 30 + ); + this.clock.tick(100); + assert.isFalse(isOver); + }); + + + it("mouseover -> slow move -> tooltips", function() { + mouse('mouseover', 10, 10); + for(let i=10; i<200; i+= 10) { + setTimeout( + () => mouse('mousemove', i/5, 10), + i + ); + } + this.clock.tick(200); + assert.isTrue(isOver); + }); + + it("mouseover -> fast move -> no tooltip", function() { + mouse('mouseover', 10, 10); + for(let i=10; i<200; i+= 10) { + setTimeout( + () => mouse('mousemove', i, 10), + i + ); + } + this.clock.tick(200); + assert.isFalse(isOver); + }); + +}); diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/hoverIntent.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/hoverIntent.js new file mode 100644 index 00000000..29198d8e --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/hoverIntent.js @@ -0,0 +1,50 @@ +'use strict'; + +// Here's a brief sketch of the class +// with things that you'll need anyway +class HoverIntent { + + constructor({ + sensitivity = 0.1, // speed less than 0.1px/ms means "hovering over an element" + interval = 100, // measure mouse speed once per 100ms: calculate the distance between previous and next points + elem, + over, + out + }) { + this.sensitivity = sensitivity; + this.interval = interval; + this.elem = elem; + this.over = over; + this.out = out; + + // make sure "this" is the object in event handlers. + this.onMouseMove = this.onMouseMove.bind(this); + this.onMouseOver = this.onMouseOver.bind(this); + this.onMouseOut = this.onMouseOut.bind(this); + + // assign the handlers + elem.addEventListener("mouseover", this.onMouseOver); + elem.addEventListener("mouseout", this.onMouseOut); + + // continue from this point + + } + + onMouseOver(event) { + /* ... */ + } + + onMouseOut(event) { + /* ... */ + } + + onMouseMove(event) { + /* ... */ + } + + + destroy() { + /* your code to "disable" the functionality, remove all handlers */ + }; + +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/index.html new file mode 100644 index 00000000..702b79f9 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/index.html @@ -0,0 +1,39 @@ + + + + + + Document + + + + + + + + +
+ 12 : + 30 : + 00 +
+ + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/style.css new file mode 100644 index 00000000..980e9457 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/style.css @@ -0,0 +1,28 @@ +.hours { + color: red; +} + +.minutes { + color: green; +} + +.seconds { + color: blue; +} + +.clock { + border: 1px dashed black; + padding: 5px; + display: inline-block; + background: yellow; + position: absolute; + left: 0; + top: 0; +} + +.tooltip { + position: absolute; + background: #eee; + border: 1px brown solid; + padding: 3px; +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/test.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/test.js new file mode 100644 index 00000000..44369d8c --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/test.js @@ -0,0 +1,98 @@ +'use strict'; + +describe("hoverIntent", function() { + + function mouse(eventType, x, y, options) { + let eventOptions = Object.assign({ + bubbles: true, + clientX: x, + clientY: y, + pageX: x, + pageY: y, + target: elem + }, options || {}); + + elem.dispatchEvent(new MouseEvent(eventType, eventOptions)); + } + + + let isOver; + let hoverIntent; + + + before(function() { + this.clock = sinon.useFakeTimers(); + }); + + after(function() { + this.clock.restore(); + }); + + + beforeEach(function() { + isOver = false; + + hoverIntent = new HoverIntent({ + elem: elem, + over: function() { + isOver = true; + }, + out: function() { + isOver = false; + } + }); + }) + + afterEach(function() { + if (hoverIntent) { + hoverIntent.destroy(); + } + }) + + it("mouseover -> immediately no tooltip", function() { + mouse('mouseover', 10, 10); + assert.isFalse(isOver); + }); + + it("mouseover -> pause shows tooltip", function() { + mouse('mouseover', 10, 10); + this.clock.tick(100); + assert.isTrue(isOver); + }); + + it("mouseover -> fast mouseout no tooltip", function() { + mouse('mouseover', 10, 10); + setTimeout( + () => mouse('mouseout', 300, 300, { relatedTarget: document.body}), + 30 + ); + this.clock.tick(100); + assert.isFalse(isOver); + }); + + + it("mouseover -> slow move -> tooltips", function() { + mouse('mouseover', 10, 10); + for(let i=10; i<200; i+= 10) { + setTimeout( + () => mouse('mousemove', i/5, 10), + i + ); + } + this.clock.tick(200); + assert.isTrue(isOver); + }); + + it("mouseover -> fast move -> no tooltip", function() { + mouse('mouseover', 10, 10); + for(let i=10; i<200; i+= 10) { + setTimeout( + () => mouse('mousemove', i, 10), + i + ); + } + this.clock.tick(200); + assert.isFalse(isOver); + }); + +}); diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md new file mode 100644 index 00000000..5c1846a9 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md @@ -0,0 +1,47 @@ +importance: 5 + +--- + +# "Smart" tooltip + +Write a function that shows a tooltip over an element only if the visitor moves the mouse *over it*, but not *through it*. + +In other words, if the visitor moves the mouse on the element and stopped -- show the tooltip. And if he just moved the mouse through fast, then no need, who wants extra blinking? + +Technically, we can measure the mouse speed over the element, and if it's slow then we assume that it comes "over the element" and show the tooltip, if it's fast -- then we ignore it. + +Make a universal object `new HoverIntent(options)` for it. With `options`: + +- `elem` -- element to track. +- `over` -- a function to call if the mouse is slowly moving the element. +- `out` -- a function to call when the mouse leaves the element (if `over` was called). + +An example of using such object for the tooltip: + +```js +// a sample tooltip +let tooltip = document.createElement('div'); +tooltip.className = "tooltip"; +tooltip.innerHTML = "Tooltip"; + +// the object will track mouse and call over/out +new HoverIntent({ + elem, + over() { + tooltip.style.left = elem.getBoundingClientRect().left + 'px'; + tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px'; + document.body.append(tooltip); + }, + out() { + tooltip.remove(); + } +}); +``` + +The demo: + +[iframe src="solution" height=140] + +If you move the mouse over the "clock" fast then nothing happens, and if you do it slow or stop on them, then there will be a tooltip. + +Please note: the tooltip doesn't "blink" when the cursor between the clock subelements. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md new file mode 100644 index 00000000..3fdde9ed --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/article.md @@ -0,0 +1,186 @@ +# Moving: mouseover/out, mouseenter/leave + +Let's dive into more details about events that happen when mouse moves between elements. + +[cut] + +## Mouseover/mouseout, relatedTarget + +The `mouseover` event occurs when a mouse pointer comes over an element, and `mouseout` -- when it leaves. + +![](mouseover-mouseout.png) + +These events are special, because they have a `relatedTarget`. + +For `mouseover`: + +- `event.target` -- is the element where the mouse came over. +- `event.relatedTarget` -- is the element from which the mouse came. + +For `mouseout` the reverse: + +- `event.target` -- is the element that mouse left. +- `event.relatedTarget` -- is the new under-the-pointer element (that mouse left for). + +```online +In the example below each face feature is an element. When you move the mouse, you can see mouse events in the text area. + +Each event has the information about where the element came and where it came from. + +[codetabs src="mouseoverout" height=280] +``` + +```warn header="`relatedTarget` can be `null`" +The `relatedTarget` property can be `null`. + +That's normal and just means that the mouse came not from another element, but from out of the window. Or that it left the window. + +We should keep that possibility in mind when using `event.relatedTarget` in our code. If we access `event.relatedTarget.tagName`, then there will be an error. +``` + +## Events frequency + +The `mousemove` event triggers when the mouse moves. But that doesn't mean that every pixel leads to an event. + +The browser checks the mouse position from time to time. And if he notices changes then triggers the events. + +That means that if the visitor is moving the mouse very fast then DOM-elements may be skipped: + +![](mouseover-mouseout-over-elems.png) + +If the mouse moves very fast from `#FROM` to `#TO` elements as painted above, then intermediate `
` (or some of them) may be skipped. The `mouseout` event may trigger on `#FROM` and then immediately `mouseover` on `#TO`. + +In practice that's helpful, because if there may be many intermediate elements. We don't really want to process in and out of each one. + +From the other side, we should keep in mind that we can't assume that the mouse slowly moves from one event to another. No, it can "jump". + +In particular it's possible that the cursor jumps right inside the middle of the page from out of the window. And `relatedTarget=null`, because it came from "nowhere": + +![](mouseover-mouseout-from-outside.png) + +
+In case of a fast move, intermediate elements may trigger no events. But if the mouse enters the element (`mouseover`), when we're guaranteed to have `mouseout` when it leaves it. +
+ +```online +Check it out "live" on a teststand below. + +The HTML is two nested `
` elements. If you move the mouse fast over them, then there may be no events at all, or maybe only the red div triggers events, or maybe the green one. + +Also try to move the pointer over the red `div`, and then move it out quickly down through the green one. If the movement is fast enough then the parent element is ignored. + +[codetabs height=360 src="mouseoverout-fast"] +``` + +## "Extra" mouseout when leaving for a child + +Imagine -- a mouse pointer entered an element. The `mouseover` triggered. Then the cursor goes into a child element. The interesting fact is that `mouseout` triggers in that case. The cursor is still in the element, but we have a `mouseout` from it! + +![](mouseover-to-child.png) + +That seems strange, but can be easily explained. + +**According to the browser logic, the mouse cursor may be only over a *single* element at any time -- the most nested one (and top by z-index).** + +So if it goes to another element (even a descendant), then it leaves the previous one. That simple. + +There's a funny consequence that we can see on the example below. + +The red `
` is nested inside the blue one. The blue `
` has `mouseover/out` handlers that log all events in the textarea below. + +Try entering the blue element and then moving the mouse on the red one -- and watch the events: + +[codetabs height=360 src="mouseoverout-child"] + +1. On entering the blue one -- we get `mouseover [target: blue]`. +2. Then after moving from the blue to the red one -- we get `mouseout [target: blue]` (left the parent). +3. ...And immediately `mouseover [target: red]`. + +So, for a handler that does not take `target` into account, it looks like we left the parent in `mouseout` in `(2)` and returned back to it by `mouseover` in `(3)`. + +If we perform some actions on entering/leaving the element, then we'll get a lot of extra "false" runs. For simple stuff may be unnoticeable. For complex things that may bring unwanted side-effects. + +We can fix it by using `mouseenter/mouseleave` events instead. + +## Events mouseenter and mouseleave + +Events `mouseenter/mouseleave` are like `mouseover/mouseout`. They also trigger when the mouse pointer enters/leaves the element. + +But there are two differences: + +1. Transitions inside the element are not counted. +2. Events `mouseenter/mouseleave` do not bubble. + +These events are intuitively very clear. + +When the pointer enters an element -- the `mouseenter` triggers, and then doesn't matter where it goes while inside the element. The `mouseleave` event only triggers when the cursor leaves it. + +If we make the same example, but put `mouseenter/mouseleave` on the blue `
`, and do the same -- we can see that events trigger only on entering and leaving the blue `
`. No extra events when going to the red one and back. Children are ignored. + +[codetabs height=340 src="mouseleave"] + +## Event delegation + +Events `mouseenter/leave` are very simple and easy to use. But they do not bubble. So we can't use event delegation with them. + +Imagine we want to handle mouse enter/leave for table cells. And there are hundreds of cells. + +The natural solution would be -- to set the handler on `` and process events there. But `mouseenter/leave` don't bubble. So if such event happens on `
`, then only a handler on that `` can catch it. + +Handlers for `mouseenter/leave` on `` only trigger on entering/leaving the whole table. It's impossible to get any information about transitions inside it. + +Not a problem -- let's use `mouseover/mouseout`. + +A simple handler may look like this: + +```js +// let's highlight cells under mouse +table.onmouseover = function(event) { + let target = event.target; + target.style.background = 'pink'; +}; + +table.onmouseout = function(event) { + let target = event.target; + target.style.background = ''; +}; +``` + +```online +[codetabs height=480 src="mouseenter-mouseleave-delegation"] +``` + +These handlers work when going from any element to any inside the table. + +But we'd like to handle only transitions in and out of `
` as a whole. And highlight the cells as a whole. We don't want to handle transitions that happen between the children of ``. + +One of solutions: + +- Remember the currently highlighted `` in a variable. +- On `mouseover` -- ignore the event if we're still inside the current ``. +- On `mouseout` -- ignore if we didn't leave the current ``. + +That filters out "extra" events when we are moving between the children of ``. + +```offline +The details are in the [full example](sandbox:mouseenter-mouseleave-delegation-2). +``` + +```online +Here's the full example with all details: + +[codetabs height=380 src="mouseenter-mouseleave-delegation-2"] + +Try to move the cursor in and out of table cells and inside them. Fast or slow -- doesn't better. Only `` as a whole is highlighted unlike the example before. +``` + + +## Итого + +У `mouseover, mousemove, mouseout` есть следующие особенности: + +- При быстром движении мыши события `mouseover, mousemove, mouseout` могут пропускать промежуточные элементы. +- События `mouseover` и `mouseout` -- единственные, у которых есть вторая цель: `relatedTarget` (`toElement/fromElement` в IE). +- События `mouseover/mouseout` подразумевают, что курсор находится над одним, самым глубоким элементом. Они срабатывают при переходе с родительского элемента на дочерний. + +События `mouseenter/mouseleave` не всплывают и не учитывают переходы внутри элемента. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/index.html new file mode 100755 index 00000000..7de45fc4 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/index.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bagua Chart: Direction, Element, Color, Meaning
Northwest +
Metal +
Silver +
Elders +
North +
Water +
Blue +
Change +
Northeast +
Earth +
Yellow +
Direction +
West +
Metal +
Gold +
Youth +
Center +
All +
Purple +
Harmony +
East +
Wood +
Blue +
Future +
Southwest +
Earth +
Brown +
Tranquility +
South +
Fire +
Orange +
Fame +
Southeast +
Wood +
Green +
Romance +
+ + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js new file mode 100755 index 00000000..c35051a0 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/script.js @@ -0,0 +1,46 @@ +// элемент TD, внутри которого сейчас курсор +var currentElem = null; + +table.onmouseover = function(event) { + if (currentElem) { + // перед тем, как зайти в новый элемент, курсор всегда выходит из предыдущего + // + // если мы еще не вышли, значит это переход внутри элемента, отфильтруем его + return; + } + + // посмотрим, куда пришёл курсор + var target = event.target; + + // уж не на TD ли? + while (target != this) { + if (target.tagName == 'TD') break; + target = target.parentNode; + } + if (target == this) return; + + // да, элемент перешёл внутрь TD! + currentElem = target; + target.style.background = 'pink'; +}; + + +table.onmouseout = function(event) { + // если курсор и так снаружи - игнорируем это событие + if (!currentElem) return; + + // произошёл уход с элемента - проверим, куда, может быть на потомка? + var relatedTarget = event.relatedTarget; + if (relatedTarget) { // может быть relatedTarget = null + while (relatedTarget) { + // идём по цепочке родителей и проверяем, + // если переход внутрь currentElem - игнорируем это событие + if (relatedTarget == currentElem) return; + relatedTarget = relatedTarget.parentNode; + } + } + + // произошло событие mouseout, курсор ушёл + currentElem.style.background = ''; + currentElem = null; +}; \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css new file mode 100755 index 00000000..61e19e36 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation-2.view/style.css @@ -0,0 +1,65 @@ +#text { + display: block; + height: 100px; + width: 456px; +} + +#table th { + text-align: center; + font-weight: bold; +} + +#table td { + width: 150px; + white-space: nowrap; + text-align: center; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 12px; +} + +#table .nw { + background: #999; +} + +#table .n { + background: #03f; + color: #fff; +} + +#table .ne { + background: #ff6; +} + +#table .w { + background: #ff0; +} + +#table .c { + background: #60c; + color: #fff; +} + +#table .e { + background: #09f; + color: #fff; +} + +#table .sw { + background: #963; + color: #fff; +} + +#table .s { + background: #f60; + color: #fff; +} + +#table .se { + background: #0c3; + color: #fff; +} + +#table .highlight { + background: red; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/index.html new file mode 100755 index 00000000..eedd3871 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/index.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bagua Chart: Direction, Element, Color, Meaning
Northwest +
Metal +
Silver +
Elders +
North +
Water +
Blue +
Change +
Northeast +
Earth +
Yellow +
Direction +
West +
Metal +
Gold +
Youth +
Center +
All +
Purple +
Harmony +
East +
Wood +
Blue +
Future +
Southwest +
Earth +
Brown +
Tranquility +
South +
Fire +
Orange +
Fame +
Southeast +
Wood +
Green +
Romance +
+ + + + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/script.js new file mode 100755 index 00000000..f0fa2d35 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/script.js @@ -0,0 +1,13 @@ +table.onmouseover = function(event) { + var target = event.target; + target.style.background = 'pink'; + text.value += "mouseover " + target.tagName + "\n"; + text.scrollTop = text.scrollHeight; +}; + +table.onmouseout = function(event) { + var target = event.target; + target.style.background = ''; + text.value += "mouseout " + target.tagName + "\n"; + text.scrollTop = text.scrollHeight; +}; \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css new file mode 100755 index 00000000..61e19e36 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseenter-mouseleave-delegation.view/style.css @@ -0,0 +1,65 @@ +#text { + display: block; + height: 100px; + width: 456px; +} + +#table th { + text-align: center; + font-weight: bold; +} + +#table td { + width: 150px; + white-space: nowrap; + text-align: center; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 12px; +} + +#table .nw { + background: #999; +} + +#table .n { + background: #03f; + color: #fff; +} + +#table .ne { + background: #ff6; +} + +#table .w { + background: #ff0; +} + +#table .c { + background: #60c; + color: #fff; +} + +#table .e { + background: #09f; + color: #fff; +} + +#table .sw { + background: #963; + color: #fff; +} + +#table .s { + background: #f60; + color: #fff; +} + +#table .se { + background: #0c3; + color: #fff; +} + +#table .highlight { + background: red; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/index.html new file mode 100755 index 00000000..472120e4 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/index.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bagua Chart: Direction, Element, Color, Meaning
Northwest +
Metal +
Silver +
Elders +
North +
Water +
Blue +
Change +
Northeast +
Earth +
Yellow +
Direction +
West +
Metal +
Gold +
Youth +
Center +
All +
Purple +
Harmony +
East +
Wood +
Blue +
Future +
Southwest +
Earth +
Brown +
Tranquility +
South +
Fire +
Orange +
Fame +
Southeast +
Wood +
Green +
Romance +
+ + + + + + + + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/script.js new file mode 100755 index 00000000..d9fac564 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/script.js @@ -0,0 +1,6 @@ +table.onmouseenter = table.onmouseleave = log; + +function log(event) { + text.value += event.type + ' [target: ' + event.target.tagName + ']\n'; + text.scrollTop = text.scrollHeight; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css new file mode 100755 index 00000000..61e19e36 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave-table.view/style.css @@ -0,0 +1,65 @@ +#text { + display: block; + height: 100px; + width: 456px; +} + +#table th { + text-align: center; + font-weight: bold; +} + +#table td { + width: 150px; + white-space: nowrap; + text-align: center; + vertical-align: bottom; + padding-top: 5px; + padding-bottom: 12px; +} + +#table .nw { + background: #999; +} + +#table .n { + background: #03f; + color: #fff; +} + +#table .ne { + background: #ff6; +} + +#table .w { + background: #ff0; +} + +#table .c { + background: #60c; + color: #fff; +} + +#table .e { + background: #09f; + color: #fff; +} + +#table .sw { + background: #963; + color: #fff; +} + +#table .s { + background: #f60; + color: #fff; +} + +#table .se { + background: #0c3; + color: #fff; +} + +#table .highlight { + background: red; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html new file mode 100755 index 00000000..87fc5c63 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + +
+
+
+ + + + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js new file mode 100755 index 00000000..7b0b70b1 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/script.js @@ -0,0 +1,4 @@ +function log(event) { + text.value += event.type + ' [target: ' + event.target.id + ']\n'; + text.scrollTop = text.scrollHeight; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css new file mode 100755 index 00000000..d4a75983 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseleave.view/style.css @@ -0,0 +1,21 @@ +#blue { + background: blue; + width: 160px; + height: 160px; + position: relative; +} + +#red { + background: red; + width: 70px; + height: 70px; + position: absolute; + left: 45px; + top: 45px; +} + +#text { + display: block; + height: 100px; + width: 400px; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside.png new file mode 100644 index 00000000..3a56239b Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside@2x.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside@2x.png new file mode 100644 index 00000000..4b852fa1 Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside@2x.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems.png new file mode 100644 index 00000000..5df6be24 Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems@2x.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems@2x.png new file mode 100644 index 00000000..50a381b9 Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems@2x.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout.png new file mode 100644 index 00000000..024a1a7f Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout@2x.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout@2x.png new file mode 100644 index 00000000..4d0ca726 Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout@2x.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.png new file mode 100644 index 00000000..cdc37a2d Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child@2x.png b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child@2x.png new file mode 100644 index 00000000..3f2ec02b Binary files /dev/null and b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child@2x.png differ diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html new file mode 100755 index 00000000..20c96c0a --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + +
+
+
+ + + + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js new file mode 100755 index 00000000..98a09815 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/script.js @@ -0,0 +1,4 @@ +function mouselog(event) { + text.value += event.type + ' [target: ' + event.target.className + ']\n' + text.scrollTop = text.scrollHeight +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css new file mode 100755 index 00000000..49e3f9d6 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-child.view/style.css @@ -0,0 +1,21 @@ +.blue { + background: blue; + width: 160px; + height: 160px; + position: relative; +} + +.red { + background: red; + width: 100px; + height: 100px; + position: absolute; + left: 30px; + top: 30px; +} + +textarea { + height: 100px; + width: 400px; + display: block; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html new file mode 100755 index 00000000..1e38d693 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + +
+
Test
+
+ + + + + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js new file mode 100755 index 00000000..4a411513 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/script.js @@ -0,0 +1,47 @@ + green.onmouseover = green.onmouseout = green.onmousemove = handler; + + function handler(event) { + var type = event.type; + while (type < 11) type += ' '; + + log(type + " target=" + event.target.id) + return false; + } + + + function clearText() { + text.value = ""; + lastMessage = ""; + } + + var lastMessageTime = 0; + var lastMessage = ""; + var repeatCounter = 1; + + function log(message) { + if (lastMessageTime == 0) lastMessageTime = new Date(); + + var time = new Date(); + + if (time - lastMessageTime > 500) { + message = '------------------------------\n' + message; + } + + if (message === lastMessage) { + repeatCounter++; + if (repeatCounter == 2) { + text.value = text.value.trim() + ' x 2\n'; + } else { + text.value = text.value.slice(0, text.value.lastIndexOf('x') + 1) + repeatCounter + "\n"; + } + + } else { + repeatCounter = 1; + text.value += message + "\n"; + } + + text.scrollTop = text.scrollHeight; + + lastMessageTime = time; + lastMessage = message; + } \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css new file mode 100755 index 00000000..e6ae1a2b --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout-fast.view/style.css @@ -0,0 +1,23 @@ +#green { + height: 50px; + width: 160px; + background: green; +} + +#red { + height: 20px; + width: 110px; + background: red; + color: white; + font-weight: bold; + padding: 5px; + text-align: center; + margin: 20px; +} + +#text { + font-size: 12px; + height: 200px; + width: 360px; + display: block; +} \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/index.html new file mode 100755 index 00000000..022fd00d --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js new file mode 100755 index 00000000..8fa60cc2 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/script.js @@ -0,0 +1,21 @@ +container.onmouseover = container.onmouseout = handler; + +function handler(event) { + + function str(el) { + if (!el) return "null" + return el.className || el.tagName; + } + + log.value += event.type + ': ' + + 'target=' + str(event.target) + + ', relatedTarget=' + str(event.relatedTarget) + "\n"; + log.scrollTop = log.scrollHeight; + + if (event.type == 'mouseover') { + event.target.style.background = 'pink' + } + if (event.type == 'mouseout') { + event.target.style.background = '' + } +} diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/style.css b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/style.css new file mode 100755 index 00000000..6347cd9b --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseoverout.view/style.css @@ -0,0 +1,179 @@ +body, +html { + margin: 0; + padding: 0; +} + +#container { + border: 1px solid brown; + padding: 10px; + width: 330px; + margin-bottom: 5px; + box-sizing: border-box; +} + +#log { + height: 120px; + width: 350px; + display: block; + box-sizing: border-box; +} + +[class^="smiley-"] { + display: inline-block; + width: 70px; + height: 70px; + border-radius: 50%; + margin-right: 20px; +} + +.smiley-green { + background: #a9db7a; + border: 5px solid #92c563; + position: relative; +} + +.smiley-green .left-eye { + width: 18%; + height: 18%; + background: #84b458; + position: relative; + top: 29%; + left: 22%; + border-radius: 50%; + float: left; +} + +.smiley-green .right-eye { + width: 18%; + height: 18%; + border-radius: 50%; + position: relative; + background: #84b458; + top: 29%; + right: 22%; + float: right; +} + +.smiley-green .smile { + position: absolute; + top: 67%; + left: 16.5%; + width: 70%; + height: 20%; + overflow: hidden; +} + +.smiley-green .smile:after, +.smiley-green .smile:before { + content: ""; + position: absolute; + top: -50%; + left: 0%; + border-radius: 50%; + background: #84b458; + height: 100%; + width: 97%; +} + +.smiley-green .smile:after { + background: #84b458; + height: 80%; + top: -40%; + left: 0%; +} + +.smiley-yellow { + background: #eed16a; + border: 5px solid #dbae51; + position: relative; +} + +.smiley-yellow .left-eye { + width: 18%; + height: 18%; + background: #dba652; + position: relative; + top: 29%; + left: 22%; + border-radius: 50%; + float: left; +} + +.smiley-yellow .right-eye { + width: 18%; + height: 18%; + border-radius: 50%; + position: relative; + background: #dba652; + top: 29%; + right: 22%; + float: right; +} + +.smiley-yellow .smile { + position: absolute; + top: 67%; + left: 19%; + width: 65%; + height: 14%; + background: #dba652; + overflow: hidden; + border-radius: 8px; +} + +.smiley-red { + background: #ee9295; + border: 5px solid #e27378; + position: relative; +} + +.smiley-red .left-eye { + width: 18%; + height: 18%; + background: #d96065; + position: relative; + top: 29%; + left: 22%; + border-radius: 50%; + float: left; +} + +.smiley-red .right-eye { + width: 18%; + height: 18%; + border-radius: 50%; + position: relative; + background: #d96065; + top: 29%; + right: 22%; + float: right; +} + +.smiley-red .smile { + position: absolute; + top: 57%; + left: 16.5%; + width: 70%; + height: 20%; + overflow: hidden; +} + +.smiley-red .smile:after, +.smiley-red .smile:before { + content: ""; + position: absolute; + top: 50%; + left: 0%; + border-radius: 50%; + background: #d96065; + height: 100%; + width: 97%; +} + +.smiley-red .smile:after { + background: #d96065; + height: 80%; + top: 60%; + left: 0%; +} diff --git a/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.md b/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.md new file mode 100644 index 00000000..28e8cffa --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.md @@ -0,0 +1,4 @@ + +We have a horizontal Drag'n'Drop here. + +To position the element we use `position:relative` and slider-relative coordinates for the thumb. Here it's more convenient here than `position:absolute`. diff --git a/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.view/index.html b/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.view/index.html new file mode 100644 index 00000000..6c980c51 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.view/index.html @@ -0,0 +1,56 @@ + + + + + + + + + + +
+
+
+ + + + + diff --git a/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.view/style.css b/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.view/style.css new file mode 100644 index 00000000..9b3d3b82 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/1-slider/solution.view/style.css @@ -0,0 +1,19 @@ +.slider { + border-radius: 5px; + background: #E0E0E0; + background: linear-gradient(left top, #E0E0E0, #EEEEEE); + width: 310px; + height: 15px; + margin: 5px; +} + +.thumb { + width: 10px; + height: 25px; + border-radius: 3px; + position: relative; + left: 10px; + top: -5px; + background: blue; + cursor: pointer; +} diff --git a/2-ui/3-event-details/4-drag-and-drop/1-slider/source.view/index.html b/2-ui/3-event-details/4-drag-and-drop/1-slider/source.view/index.html new file mode 100644 index 00000000..a9a545c0 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/1-slider/source.view/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+
+
+ + + + + diff --git a/2-ui/3-event-details/4-drag-and-drop/1-slider/source.view/style.css b/2-ui/3-event-details/4-drag-and-drop/1-slider/source.view/style.css new file mode 100644 index 00000000..9b3d3b82 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/1-slider/source.view/style.css @@ -0,0 +1,19 @@ +.slider { + border-radius: 5px; + background: #E0E0E0; + background: linear-gradient(left top, #E0E0E0, #EEEEEE); + width: 310px; + height: 15px; + margin: 5px; +} + +.thumb { + width: 10px; + height: 25px; + border-radius: 3px; + position: relative; + left: 10px; + top: -5px; + background: blue; + cursor: pointer; +} diff --git a/2-ui/3-event-details/4-drag-and-drop/1-slider/task.md b/2-ui/3-event-details/4-drag-and-drop/1-slider/task.md new file mode 100644 index 00000000..0c6da4e2 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/1-slider/task.md @@ -0,0 +1,16 @@ +importance: 5 + +--- + +# Slider + +Create a slider: + +[iframe src="solution" height=60 border=1] + +Drag the blue thumb with the mouse and move it. + +Important details: + +- When the mouse button is pressed, during the dragging the mouse may go over or below the slider. The slider will still work (convenient for the user). +- If the mouse moves very fast to the left or to the right, the thumb should stop exactly at the edge. diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.md b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.md new file mode 100644 index 00000000..7178a96f --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.md @@ -0,0 +1,5 @@ +В решении этой задачи для переноса мы используем координаты относительно окна и `position:fixed`. Так проще. + +А по окончании -- прибавляем прокрутку и делаем `position:absolute`, чтобы элемент был привязан к определённому месту в документе, а не в окне. Можно было и сразу `position:absolute` и оперировать в абсолютных координатах, но код был бы немного длиннее. + +Детали решения расписаны в комментариях в исходном коде. \ No newline at end of file diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/index.html b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/index.html new file mode 100644 index 00000000..a0a9b9bd --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + +

Расставьте супергероев по полю.

+ +

Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.

+ +

Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.

+ +

Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.

+ +
+ +
+ +
+
+
+
+
+
+ + + +
+ + + + + diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/soccer.css b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/soccer.css new file mode 100644 index 00000000..9883b812 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/soccer.css @@ -0,0 +1,48 @@ +html, +body { + margin: 0; + padding: 0; +} + +#field { + background: url(https://js.cx/drag-heroes/field.png); + width: 800px; + height: 600px; + float: left; +} +/* герои и мяч (переносимые элементы) */ + +.hero { + background: url(https://js.cx/drag-heroes/heroes.png); + width: 130px; + height: 128px; + float: left; +} + +#hero1 { + background-position: 0 0; +} + +#hero2 { + background-position: 0 -128px; +} + +#hero3 { + background-position: -120px 0; +} + +#hero4 { + background-position: -125px -128px; +} + +#hero5 { + background-position: -248px -128px; +} + +#hero6 { + background-position: -244px 0; +} + +.draggable { + cursor: pointer; +} diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/soccer.js b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/soccer.js new file mode 100644 index 00000000..6c3daa5b --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/solution.view/soccer.js @@ -0,0 +1,99 @@ +document.addEventListener('mousedown', function(event) { + + let dragElement = event.target.closest('.draggable'); + + if (!dragElement) return; + + event.preventDefault(); + let coords, shiftX, shiftY; + + startDrag(event.clientX, event.clientY); + + document.addEventListener('mousemove', onMouseMove); + + dragElement.onmouseup = function() { + finishDrag(); + }; + + function onMouseMove(event) { + moveAt(event.clientX, event.clientY); + } + + // on drag start: + // remember the initial shift + // move the element position:fixed and a direct child of body + function startDrag(clientX, clientY) { + + shiftX = clientX - dragElement.getBoundingClientRect().left; + shiftY = clientY - dragElement.getBoundingClientRect().top; + + dragElement.style.position = 'fixed'; + + document.body.append(dragElement); + + moveAt(clientX, clientY); + }; + + // switch to absolute coordinates at the end, to fix the element in the document + function finishDrag() { + dragElement.style.top = parseInt(dragElement.style.top) + pageYOffset + 'px'; + dragElement.style.position = 'absolute'; + + document.removeEventListener('mousemove', onMouseMove); + dragElement.onmouseup = null; + } + + function moveAt(clientX, clientY) { + // new window-relative coordinates + let newX = clientX - shiftX; + let newY = clientY - shiftY; + + // check if the new coordinates are below the bottom window edge + let newBottom = newY + dragElement.offsetHeight; // new bottom + + // below the window? let's scroll the page + if (newBottom > document.documentElement.clientHeight) { + // window-relative coordinate of document end + let docBottom = document.documentElement.getBoundingClientRect().bottom; + + // scroll the document down by 10px has a problem + // it can scroll beyond the end of the document + // Math.min(how much left to the end, 10) + let scrollY = Math.min(docBottom - newBottom, 10); + + // calculations are imprecise, there may be rounding errors that lead to scrolling up + // that should be impossible, fix that here + if (scrollY < 0) scrollY = 0; + + window.scrollBy(0, scrollY); + + // a swift mouse move make put the cursor beyond the document end + // if that happens - + // limit the new Y by the maximally possible (right at the bottom of the document) + newY = Math.min(newY, document.documentElement.clientHeight - dragElement.offsetHeight); + } + + // check if the new coordinates are above the top window edge (similar logic) + if (newY < 0) { + // scroll up + let scrollY = Math.min(-newY, 10); + if (scrollY < 0) scrollY = 0; // check precision errors + + window.scrollBy(0, -scrollY); + // a swift mouse move can put the cursor beyond the document start + newY = Math.max(newY, 0); // newY may not be below 0 + } + + + // limit the new X within the window boundaries + // there's no scroll here so it's simple + if (newX < 0) newX = 0; + if (newX > document.documentElement.clientWidth - dragElement.offsetWidth) { + newX = document.documentElement.clientWidth - dragElement.offsetWidth; + } + + dragElement.style.left = newX + 'px'; + dragElement.style.top = newY + 'px'; + } + +}); diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/index.html b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/index.html new file mode 100644 index 00000000..35c66686 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + +

Расставьте супергероев по полю.

+ +

Супергерои и мяч -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.

+ +

Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.

+ +

Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.

+ +
+ +
+ +
+
+
+
+
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/soccer.css b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/soccer.css new file mode 100644 index 00000000..d72bd8c9 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/soccer.css @@ -0,0 +1,48 @@ +html, +body { + margin: 0; + padding: 0; +} + +#field { + background: url(https://js.cx/drag-heroes/field.png); + width: 800px; + height: 600px; + float: left; +} +/* герои и мяч (переносимые элементы) */ + +.hero { + background: url(https://js.cx/drag-heroes/heroes.png); + width: 130px; + height: 128px; + float: left; +} + +#hero1 { + background-position: 0 0; +} + +#hero2 { + background-position: 0 -128px; +} + +#hero3 { + background-position: -120px 0; +} + +#hero4 { + background-position: -125px -128px; +} + +#hero5 { + background-position: -248px -128px; +} + +#hero6 { + background-position: -244px 0; +} + +.draggable { + cursor: pointer; +} \ No newline at end of file diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/soccer.js b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/soccer.js new file mode 100644 index 00000000..f5e11556 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/source.view/soccer.js @@ -0,0 +1 @@ +// Ваш код \ No newline at end of file diff --git a/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/task.md b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/task.md new file mode 100644 index 00000000..78b39823 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/2-drag-heroes/task.md @@ -0,0 +1,21 @@ +importance: 5 + +--- + +# Расставить супергероев по полю + +В этой задаче вы можете проверить своё понимание сразу нескольких аспектов Drag'n'Drop. + +Сделайте так, чтобы элементы с классом `draggable` можно было переносить мышкой. По окончании переноса элемент остаётся на том месте в документе, где его положили. + +Требования к реализации: + +- Должен быть 1 обработчик на `document`, использующий делегирование. +- Если элементы подносят к вертикальным краям окна -- оно должно прокручиваться вниз/вверх. +- Горизонтальной прокрутки в этой задаче не существует. +- Элемент при переносе, даже при резких движениях мышкой, не должен попасть вне окна. + +Футбольное поле в этой задаче слишком большое, чтобы показывать его здесь, поэтому откройте его, кликнув по ссылке ниже. Там же и подробное описание задачи (осторожно, винни-пух и супергерои!). + +[demo src="solution"] + diff --git a/2-ui/3-event-details/4-drag-and-drop/article.md b/2-ui/3-event-details/4-drag-and-drop/article.md new file mode 100644 index 00000000..4c573c42 --- /dev/null +++ b/2-ui/3-event-details/4-drag-and-drop/article.md @@ -0,0 +1,296 @@ +# Mouse: Drag'n'Drop [TODO] + +Drag'n'Drop is a great interface solution. Taking something, dragging and dropping is a clear and simple way to do many things, from copying and moving (see file managers) to ordering (drop into cart). + +In the modern HTML standard there's a [section about Drag Events](https://html.spec.whatwg.org/multipage/interaction.html#dnd). + +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 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. + +So here we'll see how to implement Drag'n'Drop using mouse events. Not that hard either. + +## Drag'n'Drop algorithm + +The basic Drag'n'Drop algorithm looks like this: + +1. Catch `mousedown` on a draggable element. +2. Prepare the element to moving (maybe create a copy of it or whatever). +3. Then on `mousemove` move it by changing `left/top` and `position:absolute`. +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. + +Here's the algorithm for drag'n'drop of a ball: + +```js +ball.onmousedown = function(event) { // (1) start the process + + // (2) prepare to moving: make absolute and on top by z-index + ball.style.position = 'absolute'; + ball.style.zIndex = 1000; + document.body.append(ball); + // ...and put that absolutely positioned ball under the cursor + + moveAt(event.pageX, event.pageY); + + // centers the ball at (pageX, pageY) coordinates + function moveAt(pageX, pageY) { + ball.style.left = pageX - ball.offsetWidth / 2 + 'px'; + ball.style.top = pageY - ball.offsetHeight / 2 + 'px'; + } + + function onMouseMove(event) { + moveAt(event.pageX, event.pageY); + } + + // (3) move the ball on mousemove + document.addEventListener('mousemove', onMouseMove); + + // (4) drop the ball, remove unneeded handlers + ball.onmouseup = function() { + document.removeEventListener('mousemove', onMouseMove); + ball.onmouseup = null; + }; + +}; +``` + +If we run the code, we can notice something strange. On the beginning of the drag'n'drop, the ball "forks": we start to dragging it's "clone". + +```online +Here's an example in action: + +[iframe src="ball" height=230] + +Try to drag'n'drop the mouse and you'll see the strange 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. + +To disable it: + +```js +ball.ondragstart = function() { + return false; +}; +``` + +Now everything will be all right. + +```online +In action: + +[iframe src="ball2" height=230] +``` + +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). + +So we should listen on `document` to catch it. + +## Correct positioning + +In the examples above the ball is always centered under the pointer: + +```js +ball.style.left = pageX - ball.offsetWidth / 2 + 'px'; +ball.style.top = pageY - ball.offsetHeight / 2 + 'px'; +``` + +Not bad, but there's a side-effect. To initiate the drag'n'drop can we `mousedown` anywhere on the ball. If do it at the edge, then the ball suddenly "jumps" to become centered. + +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. + +![](ball_shift.png) + +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. + + To get these shifts we can substract the coordinates: + + ```js + // onmousedown + let shiftX = event.clientX - ball.getBoundingClientRect().left; + let shiftY = event.clientY - ball.getBoundingClientRect().top; + ``` + + Please note that there's no method to get document-relative coordinates in JavaScript, so we use window-relative coordinates here. + +2. Then while dragging we position the ball on the same shift relative to the pointer, like this: + + ```js + // onmousemove + // ball has position:absoute + ball.style.left = event.pageX - *!*shiftX*/!* + 'px'; + ball.style.top = event.pageY - *!*shiftY*/!* + 'px'; + ``` + +The final code with better positioning: + +```js +ball.onmousedown = function(event) { + +*!* + let shiftX = event.clientX - ball.getBoundingClientRect().left; + let shiftY = event.clientY - ball.getBoundingClientRect().top; +*/!* + + ball.style.position = 'absolute'; + ball.style.zIndex = 1000; + document.body.append(ball); + + moveAt(event.pageX, event.pageY); + + // centers the ball at (pageX, pageY) coordinates + function moveAt(pageX, pageY) { + ball.style.left = pageX - *!*shiftX*/!* + 'px'; + ball.style.top = pageY - *!*shiftY*/!* + 'px'; + } + + function onMouseMove(event) { + moveAt(event.pageX, event.pageY); + } + + // (3) move the ball on mousemove + document.addEventListener('mousemove', onMouseMove); + + // (4) drop the ball, remove unneeded handlers + ball.onmouseup = function() { + document.removeEventListener('mousemove', onMouseMove); + ball.onmouseup = null; + }; + +}; + +ball.ondragstart = function() { + return false; +}; +``` + +```online +In action (inside `