diff --git a/1-js/2-first-steps/9-comparison/article.md b/1-js/2-first-steps/9-comparison/article.md index 88f4504b..92570cf8 100644 --- a/1-js/2-first-steps/9-comparison/article.md +++ b/1-js/2-first-steps/9-comparison/article.md @@ -128,15 +128,16 @@ alert( +"2" > +"14" ); // false, теперь правильно ## Сравнение разных типов -При сравнении значения преобразуются к числам. Исключение: когда оба значения -- строки, тогда не преобразуются. +При сравнении значений разных типов, используется числовое преобразование. Оно применяется к обоим значениям. Например: ```js //+ run -alert( '2' > 1 ); // true -alert( '01' == 1 ); //true -alert( false == 0 ); // true, false становится 0, а true 1. +alert( '2' > 1 ); // true, сравнивается как 2 > 1 +alert( '01' == 1 ); // true, сравнивается как 1 == 1 +alert( false == 0 ); // true, false становится числом 0 +alert( true == 1); // true, так как true становится числом 1. ``` Тема преобразований типов будет продолжена далее, в главе [](/types-conversion). @@ -177,12 +178,14 @@ alert(0 === false); // false, т.к. типы различны Проблемы со специальными значениями возможны, когда к переменной применяется операция сравнения `> < <= >=`, а у неё может быть как численное значение, так и `null/undefined`. -**Интуитивно кажется, что `null/undefined` эквивалентны нулю, но это не так! Они ведут себя по-другому.** +**Интуитивно кажется, что `null/undefined` эквивалентны нулю, но это не так.** + +Они ведут себя по-другому.
    -
  1. **Значения `null` и `undefined` равны `==` друг другу и не равны чему бы то ни было ещё.** +
  2. Значения `null` и `undefined` равны `==` друг другу и не равны чему бы то ни было ещё. Это жёсткое правило буквально прописано в спецификации языка.
  3. -
  4. **При преобразовании в число `null` становится `0`, а `undefined` становится `NaN`.**
  5. +
  6. При преобразовании в число `null` становится `0`, а `undefined` становится `NaN`.
Посмотрим забавные следствия. @@ -240,4 +243,6 @@ alert(undefined == 0); // false (3)
  • Строки сравниваются побуквенно.
  • Значения разных типов приводятся к числу при сравнении, за исключением строгого равенства `===` (`!==`).
  • Значения `null` и `undefined` равны `==` друг другу и не равны ничему другому. В других сравнениях (с участием `>`,`<`) их лучше не использовать, так как они ведут себя не как `0`.
  • - \ No newline at end of file + + +Мы ещё вернёмся к теме сравнения позже, когда лучше изучим различные типы данных в JavaScript. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/solution.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/solution.md similarity index 100% rename from 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/solution.md rename to 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/solution.md diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/solution.view/index.html b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/solution.view/index.html similarity index 92% rename from 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/solution.view/index.html rename to 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/solution.view/index.html index cf4e585d..3604a891 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/solution.view/index.html +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/solution.view/index.html @@ -8,7 +8,7 @@ } .tooltip { - position:absolute; + position: fixed; z-index:100; /* подсказка должна перекрывать другие элементы */ padding: 10px 20px; @@ -66,8 +66,7 @@ var showingTooltip; document.onmouseover = function(e) { - e = e || event; - var target = e.target || e.srcElement; + var target = e.target; // ВАЖНО: mouseover может сработать сразу на потомке // минуя родителя (при быстром движении мышью) @@ -105,16 +104,14 @@ function showTooltip(text, elem) { tooltipElem.innerHTML = text; document.body.appendChild(tooltipElem); - var coords = getCoords(elem); + var coords = elem.getBoundingClientRect(); - // не вылезаем за пределы экрана - var scroll = getPageScroll(); - - var left = coords.left + (elem.offsetWidth - tooltipElem.offsetWidth)/2^0; - if (left < scroll.left) left = scroll.left; // не вылезать за левую границу экрана + var left = coords.left + (elem.offsetWidth - tooltipElem.offsetWidth)/2; + if (left < 0) left = 0; // не вылезать за левую границу экрана + // не вылезать за верхнюю границу окна var top = coords.top - tooltipElem.offsetHeight - 5; - if (top < scroll.top) { + if (top < 0) { top = coords.top + elem.offsetHeight + 5; } diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-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 similarity index 56% rename from 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/source.view/index.html rename to 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/source.view/index.html index b573bc2a..c22a0aa2 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-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 @@ -46,50 +46,5 @@ - - diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/task.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md similarity index 77% rename from 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/task.md rename to 2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md index 6d7f7400..d2ed652e 100644 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-behavior-nested-tooltip/task.md +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-behavior-nested-tooltip/task.md @@ -1,6 +1,6 @@ # Поведение "вложенная подсказка" -[importance 4] +[importance 5] Напишите JS-код, который будет показывать всплывающую подсказку над элементом, если у него есть атрибут `data-tooltip`. @@ -20,7 +20,6 @@ [iframe src="solution" height=300 border=1] -Исходный документ содержит вспомогательные функции [](#getPageScroll) и [](#getCoords). -Вы также можете использовать как заготовку решение задачи [](/task/behavior-tooltip). +Вы можете использовать как заготовку решение задачи [](/task/behavior-tooltip). diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-track-cursor-movements/solution.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-track-cursor-movements/solution.md deleted file mode 100644 index 46e823fd..00000000 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-track-cursor-movements/solution.md +++ /dev/null @@ -1,7 +0,0 @@ -Самый простой способ решения этой задачи -- замерять скорость движения курсора. То есть, при `mousemove` вычислять расстояние между текущими координатами и предыдущими, а затем делить на разницу во времени. - -Когда скорость будет очень маленькой, например 5 пикселей за 100 миллисекунд -- можно считать, что курсор остановился (некоторое дрожание может присутствовать) и обработать этот факт. - -Обработчик `mousemove` может стоять на всём документе, либо на контейнере, который включает в себя интересующие нас элементы. - - diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-track-cursor-movements/task.md b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-track-cursor-movements/task.md deleted file mode 100644 index 30ca50ab..00000000 --- a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/1-track-cursor-movements/task.md +++ /dev/null @@ -1,9 +0,0 @@ -# Как отследить прекращение движения курсора? - -[importance 5] - -Представим ситуацию -- посетитель быстро проводит мышью над элементами и останавливается на интересном ему. - -Нам надо запустить код (например открыть пункт меню) для того, на котором он остановился, а те элементы, над которыми он быстро провёл мышь, но на которых не остановился -- игнорировать, даже если события мыши на них произошли. - -Как бы вы решали такую задачу? 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..8f27252c --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.md @@ -0,0 +1,20 @@ +Будем замерять скорость движения курсора. + +Для этого можно запустить `setInterval`, который каждые 100мс (или другой интервал) будет сравнивать текущие координаты курсора с предыдущими и, если расстояние пройдено маленькое, считаем, что посетитель "навёл указатель на элемент", вызвать `options.over`. + +В браузере нет способа "просто получить" текущие координаты. Это может сделать обработчик события, в данном случае `mousemove`. Поэтому нужно будет поставить обработчик на `mousemove` и при каждом движении запоминать текущие координаты, чтобы `setInterval` мог раз в 100мс сравнивать их. + +Можно обойтись и без `setInterval` -- сравнивать координаты при каждом срабатывании `mousemove`. Если передвинулись на маленькое расстояние с последнего `mousemove` -- это "наведение на элемент", а на большое -- игнорируем. Вариант с `setInterval` теоретически надёжнее, но на практике и один `mousemove` работает. + +Чтобы наш код не срабатывал чересчур часто, мы будем начинать анализ координат при заходе на элемент, а заканчивать -- при выходе с него. + +Если выход осуществлён, и при этом на элементе зафиксировано "состояние наведения", то нужно вызвать соответствующий обработчик `options.out`. + +Чтобы точно отловить момент входа и выхода, без учёта подэлементов (во избежание мигания), можно использовать `mouseenter/mouseleave`. + +В решении, предложенном ниже, однако, используется `mouseover/mouseout`, так как это позволит легко "прикрутить" к такому объекту делегирование, если потребуется. А, чтобы не было лишних срабатываний, лишние переходы отфильтровываются. + + + + + 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..3f683970 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/hoverIntent.js @@ -0,0 +1,77 @@ + +function HoverIntent(options) { + + options = Object.create(options); // not to modify the object + options.interval = options.interval || 100; + + // скорость меньше 1px/ms считается остановкой над элементом + options.sensitivity = options.sensitivity || 0.1; + var elem = options.elem; + + // instantiate variables + // cX, cY = current X and Y position of mouse, updated by mousemove event + // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval + var cX, cY, pX, pY, cTime, pTime; + var checkSpeedInterval; + var isOverElement; + var isHover; + + // A private function for handling mouse 'hovering' + elem.addEventListener("mouseover", function(event) { + + if (isOverElement) { + // если мы и так над элементом, то это всплывший переход внутри него + // мы и так уже замеряем скорость, поэтому этот переход лишний + return; + } + + isOverElement = true; + + // при каждом движении мыши mousemove мы будем вычислять расстояние между + // предыдущими и текущими координатами курсора + // если оно меньше sensivity, то скорость маленькая и это наведение на элемент + // pX, pY - "предыдущие" координаты + pX = event.pageX; + pY = event.pageY; + pTime = Date.now(); + + elem.addEventListener('mousemove', onMouseMove); + }); + + elem.addEventListener("mouseout", function(event) { + // если ушли вовне элемента + if (event.relatedTarget && !elem.contains(event.relatedTarget)) { + isOverElement = false; + elem.removeEventListener('mousemove', onMouseMove); + if (isHover) { + // если была остановка над элементом + options.out.call(elem, event); + isHover = false; + } + } + }); + + function onMouseMove(event) { + cX = event.pageX; + cY = event.pageY; + cTime = Date.now(); + + if (pTime == cTime) return; // когда mousemove вместе с mouseover + + var speed = Math.sqrt(Math.pow(pX - cX, 2) + Math.pow(pY - cY, 2)) / (cTime - pTime); + + if (speed < options.sensitivity) { + // если с предыдущей позиции меньше sensivity дистанция, то "остановка на элементе" + elem.removeEventListener("mousemove", onMouseMove); + isHover = true; + options.over.call(elem, event); + } else { + // следующее измерение с текущей точки + pX = cX; + pY = cY; + pTime = cTime; + } + } + +} + 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..ae12de67 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/index.html @@ -0,0 +1,44 @@ + + + + + Document + + + + + + +
    + 12 + : + 30 + : + 00 +
    + + + + + + + + \ No newline at end of file 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..a04e5f9d --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/solution.view/style.css @@ -0,0 +1,26 @@ +.hours { + color: red; +} + +.minutes { + color: green; +} + +.seconds { + color: blue; +} + +.clock { + border: 1px dashed black; + padding: 5px; + display: inline-block; + margin: 30px; + background: yellow; +} + +.tooltip { + position: absolute; + background: #eee; + border: 1px brown solid; + padding: 3px; +} \ No newline at end of file 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..8e93b486 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/hoverIntent.js @@ -0,0 +1,5 @@ + +function HoverIntent(options) { + /* ваш код */ +} + 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..ae12de67 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/index.html @@ -0,0 +1,44 @@ + + + + + Document + + + + + + +
    + 12 + : + 30 + : + 00 +
    + + + + + + + + \ No newline at end of file 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..a04e5f9d --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/source.view/style.css @@ -0,0 +1,26 @@ +.hours { + color: red; +} + +.minutes { + color: green; +} + +.seconds { + color: blue; +} + +.clock { + border: 1px dashed black; + padding: 5px; + display: inline-block; + margin: 30px; + background: yellow; +} + +.tooltip { + position: absolute; + background: #eee; + border: 1px brown solid; + padding: 3px; +} \ No newline at end of file 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..66de2788 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/2-hoverintent/task.md @@ -0,0 +1,53 @@ +# Подсказка при замедлении над элементом + +[importance 5] + +Нужно написать функцию, которая показывает подсказку при *наведении* на элемент, но не при *быстром проходе* над ним. + +То есть, если посетитель именно навёл курсор мыши на элемент и почти остановился -- подсказку показать, а если быстро провёл над ним, то не надо (зачем излишнее мигание?). + +Технически -- можно измерять скорость движения мыши над элементом, если она маленькая, то считаем, что это "наведение на элемент" (показать подсказку), если большая -- "быстрый проход мимо элемента" (не показывать). + +Задача -- сделать универсальный код, который отслеживает "наведение на элемент". + +Пусть это будет объект `new HoverIntent(options)`, который при создании принимает `options`: + + +Пример использования такого объекта для подсказки: +```js +function HoverIntent(options) { + //... ваш код ... +} + +// образец подсказки +var tooltip = document.createElement('div'); +tooltip.className = "tooltip"; +tooltip.innerHTML = "Подсказка"; + +// при "наведении на элемент" показать подсказку +new HoverIntent({ + elem: elem, + over: function() { + tooltip.style.left = this.getBoundingClientRect().left + 'px'; + tooltip.style.top = this.getBoundingClientRect().bottom + 5 + 'px'; + document.body.appendChild(tooltip); + }, + out: function() { + document.body.removeChild(tooltip); + } +}); +``` + +Демо: + +[iframe src="solution" height=110] + +Если провести мышкой над "часиками" быстро, то ничего не будет, а если медленно или остановиться на них, то появится подсказка. + +Обратите внимание -- подсказка не "мигает"" при проходе мыши внутри "часиков", по подэлементам. + + 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 index a1872698..ff73c43f 100644 --- 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 @@ -1,36 +1,65 @@ # Мышь: движение mouseover/out, mouseenter/leave В этой главе мы рассмотрим события, возникающие при движении мыши над элементами. + [cut] ## События mouseover/mouseout, свойство relatedTarget Событие `mouseover` происходит, когда мышь появляется над элементом, а `mouseout` -- когда уходит из него. -При этом мы можем узнать, с какого элемента пришла (или на какой ушла) мышь, используя дополнительное свойство `relatedTarget`. + -В случае `mouseover` оно содержит элемент, *с которого* пришла мышь, а для `mouseout` -- *на который* ушла. +При этом мы можем узнать, с какого элемента пришла (или на какой ушла) мышь, используя дополнительное свойство объекта события `relatedTarget`. +Например, в обработчике события `mouseover`: + + +Для `mouseout` всё наоборот: + + + +[online] В примере ниже вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах. [codetabs src="mouseoverout" height=220] +[/online] -Можно заметить, что в некоторых случаях значение `relatedTarget` может быть `null`. Это вполне нормально и означает, что мышь пришла из-за пределов окна (или ушла за окно). +[warn header="`relatedTarget` может быть `null`"] +Свойство `relatedTarget` может быть равно `null`. -## Частота событий mousemove и mouseover/out +Это вполне нормально и означает, что мышь пришла не с другого элемента, а из-за пределов окна (или ушла за окно). Мы обязательно должны иметь в виду такую возможность, когда пишем код, который обращается к свойствам `event.relatedTarget`. +[/warn] + +## Частота событий Событие `mousemove` срабатывает при передвижении мыши. Но это не значит, что каждый пиксель экрана порождает отдельное событие! События `mousemove` и `mouseover/mouseout` срабатывают так часто, насколько это позволяет внутренняя система взаимодействия с мышью браузера. -Это означает, что если вы двигаете мышью очень быстро, то DOM-элементы, через которые мышь проходит на большой скорости, могут быть пропущены. +Это означает, что если посетитель двигает мышью быстро, то DOM-элементы, через которые мышь проходит на большой скорости, могут быть пропущены. -К примеру: - + +При быстром движении с элемента `#FROM` до элемента `#TO`, как изображено на картинке выше -- промежуточные `
    ` будут пропущены. Сработает только событие `mouseout` на `#FROM` и `mouseover` на `#TO`. + +На практике это полезно, потому что таких промежуточных элементов может быть много, и если обрабатывать заход и уход с каждого -- дополнительные вычислительные затраты. + +С другой стороны, мы должны это понимать и не рассчитывать на то, что мышь аккуратно пройдёт с одного элемента на другой и так далее. Нет, она "прыгает". + +В частности, возможна ситуация, когда курсор прыгает в середину страницы, и при этом `relatedTarget=null`, то есть он пришёл "ниоткуда" (на самом деле извне окна): + + + +Обратим внимание ещё на такую деталь. При быстром движении курсор окажется над `#TO` сразу, даже если этот элемент глубоко в DOM. Его родители при движении сквозь них события не поймают. + +[online] Попробуйте увидеть это "вживую" на тестовом стенде ниже. Его HTML представляет собой два вложенных `div'а`. @@ -40,22 +69,44 @@ А еще попробуйте зайти курсором мыши на красный `div` и потом быстро вывести мышь из него куда-нибудь сквозь зеленый. Если движение мыши достаточно быстрое, то родительский элемент будет проигнорирован. [codetabs height=360 src="mouseoverout-fast"] +[/online] +Важно иметь в виду эту особенность событий, чтобы не написать код, который рассчитан на последовательный проход над элементами. В остальном это вполне удобно. -Важно иметь в виду эту особенность событий, чтобы не написать код, который рассчитан на последовательный проход над элементами. ## "Лишний" mouseout при уходе на потомка Представьте ситуацию -- курсор зашёл на элемент. Сработал `mouseover` на нём. Потом курсор идёт на дочерний... И, оказывается, на элементе-родителе при этом происходит `mouseout`! Как будто курсор с него ушёл, хотя он всего лишь перешёл на потомка. **При переходе на потомка срабатывает `mouseout` на родителе.** + + Это кажется странным, но легко объяснимо. **Согласно браузерной логике, курсор мыши может быть только над *одним* элементом -- самым глубоким в DOM (и верхним по z-index).** Так что если он перешел куда-нибудь, то автоматически ушёл с предыдущего элемента. Всё просто. -К чему это приводит на практике, можно увидеть в примере ниже. В нём красный `div` вложен в синий. На синем стоит обработчик, который записывает его `mouseover/mouseout`. +Самое забавное начинается чуть позже. + +Ведь события `mouseover` и `mouseout` всплывают. + +Получается, что если поставить обработчики `mouseover` и `mouseout` на `#FROM` и `#TO`, то последовательность срабатывания при переходе `#FROM` -> `#TO` будет следующей: + +
      +
    1. `mouseout` на `#FROM` (с `event.target=#FROM`, `event.relatedTarget=#TO`).
    2. +
    3. `mouseover` на `#TO` (с `event.target=#TO`, `event.relatedTarget=#FROM`).
    4. +
    5. Событие `mouseover` после срабатывания на `#TO` всплывает выше, запуская обработчики `mouseover` на родителях. Ближайший родитель -- как раз `#FROM`, то есть сработает обработчик `mouseover` на нём, с теми же значениями `target/relatedTarget`.
    6. +
    + +Если посмотреть на `1)` и `3)`, то видно, что то видно, что на `#FROM` сработает сначала `mouseout`, а затем с `#TO` всплывёт `mouseover`. + +Если по `mouseover` мы что-то показываем, а по `mouseout` -- скрываем, то может получается "мигание". + +**У обработчиков создаётся впечатление, что курсор ушёл `mouseout` с родителя, а затем тут же перешёл `mouseover` на него (за счёт всплытия `mouseover` с потомка).** + +[online] +Это можно увидеть в примере ниже. В нём красный `div` вложен в синий. На синем стоит обработчик, который записывает его `mouseover/mouseout`. Зайдите на синий элемент, а потом переведите мышь на красный -- и наблюдайте за событиями: @@ -68,12 +119,9 @@ На самом деле, обратного перехода нет. Событие `mouseover` сработало на потомке (видно по `target: red`), а затем всплыло. +[/online] -**То есть, у кода создаётся впечатление, что курсор ушёл `mouseout` с родителя, а затем тут же перешёл `mouseover` на него (за счёт всплытия `mouseover` с потомка).** - -Как это влияет на его поведение? - -Если действия при наведении и уходе курсора с родителя простые, например скрытие/показ подсказки, то можно вообще ничего не заметить. Ведь события происходят одновременно, подсказка будет скрыта по `mouseout` и тут же показана по `mouseover`. +Если действия при наведении и уходе курсора с родителя простые, например скрытие/показ подсказки, то можно вообще ничего не заметить. Ведь события происходят сразу одно за другим, подсказка будет скрыта по `mouseout` и тут же показана по `mouseover`. Если же происходит что-то более сложное, то бывает важно отследить момент "настоящего" ухода, то есть понять, когда элемент зашёл на родителя, а когда ушёл -- без учёта переходов по дочерним элементам. @@ -84,39 +132,34 @@ События `mouseenter/mouseleave` похожи на `mouseover/mouseout`. Они тоже срабатывают, когда курсор заходит на элемент и уходит с него, но с двумя отличиями.
      -
    1. При переходе на потомка курсор не уходит с родителя.
    2. +
    3. Не учитываются переходы внутри элемента.
    4. События `mouseenter/mouseleave` не всплывают.
    -Эти события более интуитивно понятны. Курсор заходит на элемент -- срабатывает `mouseenter`, а затем -- неважно, куда он внутри него переходит, `mouseleave` будет, когда курсор окажется за пределами элемента. +Эти события более интуитивно понятны. + +Курсор заходит на элемент -- срабатывает `mouseenter`, а затем -- неважно, куда он внутри него переходит, `mouseleave` будет, когда курсор окажется за пределами элемента. + +[online] Вы можете увидеть, как они работают проведя курсором над голубым `DIV'ом` ниже. Обработчик стоит только на внешнем, синем элементе. Обратите внимание -- лишних событий при переходе на красного потомка нет! [codetabs height=340 src="mouseleave"] +[/online] -## Делегирование -- проблема mouseenter/leave +## Делегирование События `mouseenter/leave` более наглядны и понятны, но они не всплывают, а значит с ними нельзя использовать делегирование. -Представьте себе, что нам нужно обработать вход/выход мыши для ячеек таблицы. А в таблице таких ячеек тысяча. +Представим, что нам нужно обработать вход/выход мыши для ячеек таблицы. А в таблице таких ячеек тысяча. -Естественное решение -- поставить обработчик на верхний элемент `` и ловить все события в нём. Но события `mouseenter/leave` не всплывают, а срабатывают именно на том элементе, на котором стоит обработчик и только на нём. +Естественное решение -- поставить обработчик на верхний элемент `
    ` и ловить все события в нём. Но события `mouseenter/leave` не всплывают, они срабатывают именно на том элементе, на котором стоит обработчик и только на нём. -Это легко видеть в примере ниже: обработчики `mouseenter/leave` стоят на `
    ` и сработают при входе-выходе из таблицы, получить из них какую-то информацию о переходах по её ячейкам не представляется возможным: - -[codetabs height=480 src="mouseleave-table"] +Если обработчики `mouseenter/leave` стоят на `
    `, то они сработают при входе-выходе из таблицы, но получить из них какую-то информацию о переходах по её ячейкам невозможно. Не беда -- воспользуемся `mouseover/mouseout`. -Но мы хотели бы, чтобы наши действия выполнялись только при входе-выходе в ячейку, без учета переходов внутри самих ячеек. - -Если нужно делегирование -- нужно использовать `mouseover/out`. - -Получится так: - -[codetabs height=450 src="mouseenter-mouseleave-delegation"] - -В этом примере код обработчиков выглядит так: +Простейший вариант обработчиков выглядит так: ```js table.onmouseover = function(event) { @@ -129,20 +172,33 @@ table.onmouseout = function(event) { target.style.background = ''; }; ``` +[online] -Пока что они срабатывают на всём подряд. Их нужно фильтровать: +[codetabs height=450 src="mouseenter-mouseleave-delegation"] + +[/online] + +В таком виде они срабатывают при переходе с любого элемента на любой. Нас же интересуют переходы строго с одной ячейки `
    ` на другую. + +Нужно фильтровать события. + +Один из вариантов:
      -
    • Вход-выход срабатывает на любых элементах, а нам нужны именно ячейки таблицы. - -Для этого по `mouseover` нужно проверять, находится ли `event.target` был внутри `
    `, это стандартная проверка при делегировании. -
  • Лишними для нас будут `mouseover/mouseout`, связанные с переходом на дочерние элементы внутри `
  • `. Проверить их можно по `event.target/event.relatedTarget`. +
  • Запоминать текущий подсвеченный `
  • ` в переменной. +
  • При `mouseover` проверять, остались ли мы внутри того же `
  • `, если да -- игнорировать. +
  • При `mouseout` проверять, если мы ушли с текущего `
  • `, а не перешли куда-то внутрь него, то игнорировать. +[offline] +Детали кода вы можете посмотреть в [edit src="mouseenter-mouseleave-delegation-2"]полном примере[/edit]. +[/offline] + +[online] Детали кода вы можете посмотреть в примере ниже, который демонстрирует этот подход: [codetabs height=450 src="mouseenter-mouseleave-delegation-2"] -Попробуйте по-разному, быстро или медленно заходить и выходить в ячейки таблицы. Обработчики `mouseover/mouseout` стоят на `table`, но при помощи делегирования корректно обрабатывают вход-выход. +Попробуйте по-разному, быстро или медленно заходить и выходить в ячейки таблицы. Обработчики `mouseover/mouseout` стоят на `table`, но при помощи делегирования корректно обрабатывают вход-выход.[/online] ## Особенности IE8- @@ -162,11 +218,13 @@ function fixRelatedTarget(e) { ## Итого У `mouseover, mousemove, mouseout` есть следующие особенности: -
      +
        +
      • При быстром движении мыши события `mouseover, mousemove, mouseout` могут пропускать промежуточные элементы.
      • События `mouseover` и `mouseout` -- единственные, у которых есть вторая цель: `relatedTarget` (`toElement/fromElement` в IE).
      • -
      • Событие `mouseout` срабатывает, когда мышь уходит с родительского элемента на дочерний. Используйте `mouseenter/mouseleave` или фильтруйте их, чтобы избежать излишнего реагирования.
      • -
      • При быстром движении мыши события `mouseover, mousemove, mouseout` могут пропускать промежуточные элементы. Мышь может моментально возникнуть над потомком, миновав при этом его родителя.
      • -
    +
  • События `mouseover/mouseout` подразумевают, что курсор находится над одним, самым глубоким элементом. Они срабатывают при переходе с родительского элемента на дочерний.
  • + + +События `mouseenter/mouseleave` не всплывают и не учитывают переходы внутри элемента. diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside.svg b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside.svg new file mode 100644 index 00000000..59fc73bb --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-from-outside.svg @@ -0,0 +1,30 @@ + + + + mouseover-mouseout-from-outside.svg + Created with bin/sketchtool. + + + + + + + + + + + + + + #TO + + + + target + + + relatedTarget = null + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems.svg b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems.svg new file mode 100644 index 00000000..9ff425b1 --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout-over-elems.svg @@ -0,0 +1,38 @@ + + + + mouseover-mouseout-over-elems.svg + Created with bin/sketchtool. + + + + + + #TO + + + + #FROM + + + + <DIV> + + + + <DIV> + + + + <DIV> + + + + mouseover + + + mouseout + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout.svg b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout.svg new file mode 100644 index 00000000..958e581f --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-mouseout.svg @@ -0,0 +1,23 @@ + + + + mouseover-mouseout.svg + Created with bin/sketchtool. + + + + + + <DIV> + + + + mouseover + + + + mouseoout + + + + \ No newline at end of file diff --git a/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg new file mode 100644 index 00000000..cd1569de --- /dev/null +++ b/2-ui/3-event-details/3-mousemove-mouseover-mouseout-mouseenter-mouseleave/mouseover-to-child.svg @@ -0,0 +1,26 @@ + + + + mouseover-to-child.svg + Created with bin/sketchtool. + + + + + + #FROM + + + + #TO + + + + mouseover + + + mouseout + + + + \ No newline at end of file diff --git a/figures.sketch b/figures.sketch index f5ba225c..d6ac3b59 100644 Binary files a/figures.sketch and b/figures.sketch differ