ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Category:

JavaScript: умная подсказка, окончание решения задачи

Начало:
1. JavaScript: умная подсказка, разбор постановки задачи
2. JavaScript: умная подсказка, подключение автоматических тестов
3. JavaScript: умная подсказка, пять автоматических тестов
4. JavaScript: умная подсказка, начало решения задачи

Завершаю разбор задачи «"Умная" подсказка» к подразделу 3.2 «Движение мыши: mouseover/out, mouseenter/leave» второй части учебника по JavaScript.

Чтобы сделать подсказку «умной», нужно анализировать скорость движения курсора мыши над целевым HTML-элементом. Этот анализ следует выполнять, начиная с момента захода курсора мыши на целевой HTML-элемент. Окончание этого анализа произойдет в двух случаях: 1) выполнятся условия для показа подсказки, заданные в задаче (после этого подсказку следует показать); 2) курсор мыши выйдет за пределы целевого HTML-элемента, а условия для показа подсказки, заданные в задаче, так ни разу и не выполнятся (в этом случае подсказка так и не будет показана).

Для измерения скорости движения курсора мыши нужно знать координаты, как минимум, двух точек, находящихся на пути движения курсора мыши. Кроме этого, нужно знать время, ушедшее на преодоление курсором мыши пути между этими двумя точками. Первую из этих точек будем называть предыдущей (в скрипте для переменных этой точки будем применять приставку «prev»), а вторую — текущей (в скрипте для переменных этой точки будем применять приставку «cur»).

Первой точкой возьмем точку, в которой возникает событие mouseover при заходе курсора мыши на целевой HTML-элемент. Этот момент обработаем в методе onMouseOver нашего класса HoverIntent, в нем же создадим свойства-переменные, в которых будем хранить координаты точки и отметку времени, в которой курсор мыши оказался в этой точке. Дополняем код из предыдущего поста (я отметил дополнения красным цветом):
onMouseOver(event) {
  // не обрабатываем переходы для потомков
  if (this.elem.contains(event.relatedTarget)) return;

  this.isShow = false;
  this.prevTimePoint = Date.now();
  this.prevX = event.clientX;
  this.prevY = event.clientY;
  
  // задание в планировщик на случай длинной паузы
  this.timerId = setTimeout(this.onMouseMove, this.interval, event);
}

Если между событием мыши mouseover и первым после него событием мыши mousemove пройдет немного времени, то всё в порядке. Ну а если курсор мыши зайдет на целевой HTML-элемент, остановится и не будет двигаться, возникнет длинная пауза? Что тогда делать? Ведь по условиям задачи при паузе через 100 миллисекунд (this.interval) нужно показать подсказку (см. второй автоматический тест).

Чтобы учесть эту ситуацию, я поставил в вышеописанном методе в планировщик браузера задание запустить метод onMouseMove через заданный в задаче интервал (см. код выше).

Если будет длинная пауза, запустится метод onMouseMove и в качестве второй точки будет взята та же точка, которая взята в качестве первой (поэтому в функцию setTimeout третьим параметром передается объект event метода onMouseOver). То есть в этом случае пройденное расстояние будем считать равным нулю, а скорость передвижения курсора мыши тоже будем считать равной нулю (нулевое расстояние делим на 100 миллисекунд). Нулевая скорость меньше заданной скорости 0,1 пикселей в миллисекунду, следовательно, нужно показать подсказку. В противном случае (если длинной паузы не будет), в качестве второй точки будет взята точка, в которой будет сгенерировано событие мыши mousemove, а задание из планировщика браузера нужно будет удалить.

Дополняем код метода onMouseMove из предыдущего поста (я отметил дополнения красным цветом):
onMouseMove(event) {
  // не обрабатываем движения мыши, если подсказка уже показана
  if (this.isShow) return;
  // удаляем задание из планировщика браузера
  clearTimeout(this.timerId);

  let curTimePoint = Date.now();
  let spentTime = curTimePoint - this.prevTimePoint;
  if (spentTime >= this.interval) {
    let passedDist = Math.sqrt(Math.pow(event.clientX - this.prevX, 2) +
                               Math.pow(event.clientY - this.prevY, 2));
    let speed = passedDist / spentTime;
    if (speed <= this.sensitivity) {
      this.over();
      this.isShow = true;
      return;
    }
    // текущие значения становятся предыдущими
    this.prevTimePoint = curTimePoint;
    this.prevX = event.clientX;
    this.prevY = event.clientY;
  }

  // задание в планировщик на случай длинной паузы
  this.timerId = setTimeout(this.onMouseMove, this.interval, event);
}

Удаление и постановка задания в планировщик браузера были уже объяснены выше. Только там имелась в виду возможность длинной паузы между событием мыши mouseover и событием мыши mousemove, а здесь — между событиями мыши mousemove и mousemove.

Пройденное расстояние passedDist определяется по теореме Пифагора (для прямоугольного треугольника квадрат длины гипотенузы равен сумме квадратов длин катетов). Нам нужно определить длину гипотенузы, поэтому берем корень квадратный от суммы квадратов длин катетов. В остальном добавленном коде разобраться несложно: названия переменных говорят сами за себя.

Отмечу последний важный момент. В случае, если при прохождении целевого HTML-элемента подсказка будет показана, то в планировщике браузера не останется невыполненных заданий, так как при показе подсказки работа метода onMouseMove завершается с помощью инструкции return; и новое задание в планировщик на случай длинной паузы между событиями мыши уже не ставится (в этом уже нет нужды, подсказка показана). Однако, если при прохождении целевого HTML-элемента подсказка не будет показана, то после последнего события мыши mousemove в планировщике браузера останется задание, которое в этом случае нам не нужно. Если это задание не удалить из планировщика, то оно выполнится после выхода курсора мыши с целевого HTML-элемента и в некоторых случаях приведет к неправильной работе скрипта.

Чтобы этого не произошло, внесем дополнение в метод onMouseOut из предыдущего поста, удаляющее ненужное задание из планировщика (я отметил дополнение красным цветом):
onMouseOut(event) {
  // не обрабатываем переходы для потомков
  if (this.elem.contains(event.relatedTarget)) return;

  // удаляем задание из планировщика (нужно, если подсказка не показана)
  clearTimeout(this.timerId);

  // скроем подсказку, если она была показана
  if (this.isShow) this.out(); console.log("out");
}

В принципе, на этом задача решена.

Но вспомним, что в четырех из пяти автоматических тестов от авторов задачи событие мыши mouseout не генерируется, а, значит, не будет запущен метод onMouseOut и последнее (уже ненужное) задание в планировщике браузера не будет удалено (для случая, когда подсказка не показана). Из-за этого некоторые тесты могут быть пройдены с отрицательным результатом. В случае взаимодействия с реальным пользователем событие мыши mouseout сгенерируется обязательно (если HTML-страница с нашим скриптом не будет внезапно закрыта, пока курсор мыши еще не ушел с целевого HTML-элемента; в случае закрытия HTML-страницы, однако, работа с подсказкой, по идее, уже не будет иметь никакого значения). Что же можно сделать для прохождения тестов?

Чтобы учесть этот случай для автоматических тестов от авторов задачи, внесем аналогичное дополнение в метод destroy из предыдущего поста, ведь этот метод запускается после каждого автоматического теста (я отметил дополнение красным цветом):
destroy() {
  // удаляем задание из планировщика (нужно, если подсказка не показана)
  clearTimeout(this.timerId);

  this.elem.removeEventListener("mouseover", this.onMouseOver);
  this.elem.removeEventListener("mouseout", this.onMouseOut);
  this.elem.removeEventListener("mousemove", this.onMouseMove);
}

Теперь «умная» подсказка работает и все автоматические тесты проходятся с положительным результатом. Можно убрать лишние инструкции console.log и раскомментировать демонстрационный пример.

Кстати, рекомендую посмотреть решение этой задачи от ее авторов. У них оно более изящное, так как они использовали для планирования заданий функцию setInterval, а не setTimeout, как я.
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments