ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Categories:

JavaScript: слайдер (ползунок)

Решил задачу «Слайдер» к подразделу 3.3 «Drag'n'Drop с событиями мыши» второй части учебника по JavaScript.

Задача несложная и интересная.

Что такое «слайдер» (это слово произошло от английского слова «slider»)? Это слово в русском языке имеет много значений, но конкретно в контексте программного обеспечения оно указывает на определенный элемент интерфейса программы. Еще к нему есть синоним: «ползунок».

На странице задачи можно рассмотреть работающий слайдер в демонстрационном примере. Так как скрипты на страницах ЖЖ запрещены (см. об этом вопрос № 14 в FAQ ЖЖ), то здесь я могу только показать картинку с изображением слайдера (это часть снимка экрана со страницы задачи):



Демонстрационный пример вставлен на страницу задачи с помощью HTML-элемента iframe. Название этого HTML-элемента является сокращением от словосочетания «inline frame». Подразумевается, что на HTML-страницу с помощью этого HTML-элемента вставляется «рамка» или «кадр» (по-английски «frame»), в котором содержится другая HTML-страница, поэтому многие переводят на русский язык фразу «inline frame» как «встроенный кадр» или «встроенный кадр с другой HTML-страницей». В данном случае на HTML-страницу задачи встраивается HTML-страница с решением этой задачи.

На картинке выше тонкой серой линией показана рамка встраиваемого кадра (часть рамки справа на картинку не вошла). Эта линия видна и на странице задачи.

Слайдер тут состоит из двух частей: вытянутого по горизонтали серого прямоугольника с закругленными краями и синей пимпочки, бегающей по серому прямоугольнику.

В коде тестовой HTML-страницы, заданной авторами задачи, слайдером (идентификатор slider) еще назван сам серый прямоугольник, по которому бегает синяя пимпочка. Этой пимпочке в том же коде дан идентификатор thumb, а я буду называть ее по-русски «бегунком» (синяя пимпочка «бегает» влево-вправо, если ее передвинуть мышью, поэтому ее называют «бегунком»).

Код тестовой HTML-страницы и стили к ней даны авторами задачи в песочнице. HTML-код слайдера на тестовой HTML-странице (слайдер изображается двумя HTML-элементами div):
<div id="slider" class="slider">
  <div class="thumb"></div>
</div>

* * *

Два замечания по поводу кода заданных авторами задачи стилей на языке CSS. Эти замечания неважны для решения задачи, но помогают разобраться в языке CSS параллельно с изучением языка JavaScript.

Во-первых, на картинке выше невооруженным глазом видно, что расстояние от слайдера до рамки кадра сверху меньше, чем расстояние от слайдера до рамки кадра слева, хотя в стилях эта разница не описана. Уменьшение расстояния от слайдера до рамки кадра сверху является результатом схлопывания внешних отступов по вертикали, я писал об этом подробно в предыдущем посте. Один из способов избавиться от такого схлопывания внешних отступов по вертикали (именно для нашего случая) — это добавить в стили следующий код на языке CSS:
html, body {
  overflow: auto; /* убираем схлопывание верхних внешних отступов */
}

Во-вторых, обратим внимание на вот этот фрагмент в стиле класса .slider:
  background: #E0E0E0;
  background: linear-gradient(left top, #E0E0E0, #EEEEEE);

Как я понимаю, по стандарту языка CSS предполагается, что в случае определения одного и того же свойства несколько раз браузер при отображении применит свойство, определенное последним. То есть, в данном случае, фон должен быть залит линейным градиентом. Слово «градиент» тут означает плавный цветовой переход от цвета #E0E0E0 к цвету #EEEEEE, а слово «линейный» означает, что цветовой переход будет производиться в одном определенном направлении. Направление в данном случае задано словами «left top», но здесь ошибка: должно быть «to left top». Имеется в виду направление от правого нижнего угла к левому верхнему углу. Если же требуется направление градиента от левого верхнего угла к нижнему правому углу, то вместо «left top» должно быть указание «to right bottom». То есть присутствие «to» обязательно, если направление градиента определяется словами.

Из-за ошибки во втором определении свойства background браузер использует первое определение этого свойства: фон будет залит цветом #E0E0E0 (можно назвать этот цвет серым). По крайней мере, так это работает в моем браузере. Если вышеуказанную ошибку исправить, будет работать заливка линейным градиентом.

* * *

Приступим к решению задачи. В принципе, в тексте подраздела учебника, к которому относится эта задача, приведен пример скрипта, дающего пользователю возможность хватать мышью, перемещать в новое место и оставлять в новом месте HTML-страницы (по-английски «Drag’n’Drop») футбольный мяч, который представлен каким-либо HTML-элементом, например, HTML-элементом div или img. То есть наша задача сводится к приспособлению имеющегося скрипта для футбольного мяча к нашему слайдеру. Я взял код первого скрипта в подразделе учебника и стал его анализировать.

Еще для анализа у нас есть демонстрационный пример на странице задачи. Я покрутил пример так и сяк, и заметил, что работа скрипта, по идее, каждый раз должна начинаться с наведения курсора мыши на синий бегунок и нажатия основной кнопки мыши («захват» объекта в алгоритме «Drag’n’Drop»). Следовательно, нам нужно повесить функцию-обработчик на событие mousedown, происходящее над HTML-элементом div CSS-класса thumb, изображающим бегунок. Пишем код:
let thumb = document.querySelector(".thumb");
thumb.onmousedown = function(event) {
    // ...
};
Все дальнейшие действия алгоритма «Drag’n’Drop» будем выполнять внутри этой функции-обработчика.

Дальше я стал изменять код по аналогии с тем, как это делали авторы учебника в скрипте с футбольным мячом:
let thumb = document.querySelector(".thumb");
thumb.onmousedown = function(event) { // вешаем обработчик захвата объекта

    // вешаем обработчик перемещения захваченного объекта
    document.addEventListener("mousemove", onMouseMove);

    // вешаем обработчик оставления захваченного объекта на новом месте
    thumb.onmouseup = function() {
        document.removeEventListener("mousemove", onMouseMove);
        thumb.onmouseup = null;
    };

};

Это еще нерабочий код (в частности, пока нигде не описана функция onMouseMove), но зато тут ясно видны все три основных действия алгоритма «Drag’n’Drop»: захват объекта, его перемещение и оставление объекта на новом месте. В качестве объекта у нас выступает бегунок слайдера. При оставлении объекта (завершающий шаг алгоритма «Drag’n’Drop») обработчики, ранее повешенные на события мыши mousemove (движение мыши) и mouseup (отпускание основной кнопки мыши), удаляются.

Добавим в код определение функции onMouseMove, в которой, собственно, реализуется движение бегунка слайдера:
let thumb = document.querySelector(".thumb");
thumb.onmousedown = function(event) {

    // функция, реализующая движение бегунка слайдера
    function onMouseMove(event) {
        thumb.style.left = event.pageX + "px";
    }

    document.addEventListener("mousemove", onMouseMove);

    thumb.onmouseup = function() {
        document.removeEventListener("mousemove", onMouseMove);
        thumb.onmouseup = null;
    };

};
Этот код уже кое-как работает, бегунок слайдера уже можно двигать, но бегунок норовит упрыгать из-под курсора мыши в сторону, а оставление бегунка на новом месте с помощью отпускания основной кнопки мыши не срабатывает. Кстати, по сравнению со скриптом футбольного мяча из обсуждаемого подраздела учебника наша задача проще: нам нужно двигать бегунок слайдера только влево или вправо по горизонтали, то есть следует менять только координату по горизотальной оси координат.

Кривую работу скрипта на этом этапе можно понять: event.pageX — это координата относительно левого верхнего угла HTML-страницы (можно было взять и event.clientX, так как для тестовой HTML-страницы, заданной в задаче, они совпадают), а в thumb.style.left нужно передать координату относительно левого верхнего угла клиентской части HTML-элемента с идентификатором slider, потому что в стиле CSS-класса .thumb есть указание position: relative; (относительное позиционирование), что означает, что HTML-элемент этого класса позиционируется относительно ближайшего родительского содержащего блока, которым является HTML-элемент div с CSS-классом .slider, так как этот HTML-элемент по умолчанию является HTML-элементом блочного типа.

Скорректируем код функции onMouseMove (я отметил изменения красным цветом):
let sliderRect = slider.getBoundingClientRect();

let thumb = document.querySelector(".thumb");
thumb.onmousedown = function(event) {

    // функция, реализующая движение бегунка слайдера
    function onMouseMove(event) {
        let x = event.pageX - sliderRect.x;
        thumb.style.left = x + "px";
    }

    document.addEventListener("mousemove", onMouseMove);

    thumb.onmouseup = function() {
        document.removeEventListener("mousemove", onMouseMove);
        thumb.onmouseup = null;
    };

};

Что тут сделано? Мы определили горизонтальную координату sliderRect.x левого верхнего угла HTML-элемента div с идентификатором slider. А вычитая ее из event.pageX, получим искомую горизонтальную координату относительно именно левого верхнего угла клиентской части HTML-элемента div с идентификатором slider. (Здесь мы не учли случай, если бы у HTML-элемента div с идентификатором slider в стиле была бы описана какая-то граница. Тогда внешний угол HTML-элемента не совпал бы с углом клиентской части HTML-элемента (в нашем случае они совпадают) и нужно было бы для нахождения искомой горизонтальной координаты вычесть из event.pageX - sliderRect.x еще и значение slider.clientLeft.)

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

Почему это происходит? Такое поведение было объяснено авторами учебника в скрипте для футбольного мяча: при «захвате» объекта мы нажимаем мышкой над HTML-элементом, изображающим объект (в нашем случае — бегунок слайдера), при этом точка, в которой происходит нажатие, смещена на какое-то расстояние от левого верхнего угла HTML-элемента, изображающего объект (бегунок слайдера). При первом передвижении мыши в нашем коде курсор мыши «прыгает» на это смещение. Чтобы не допустить этого прыжка, необходимо сохранять это смещение во время всего передвижения объекта. Меняем код (я отметил изменения красным цветом):
let sliderRect = slider.getBoundingClientRect();

let thumb = document.querySelector(".thumb");
thumb.onmousedown = function(event) {

    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.pageX - thumbRect.x;

    // функция, реализующая движение бегунка слайдера
    function onMouseMove(event) {
        let x = event.pageX - sliderRect.x - shiftX;
        thumb.style.left = x + "px";
    }

    document.addEventListener("mousemove", onMouseMove);

    thumb.onmouseup = function() {
        document.removeEventListener("mousemove", onMouseMove);
        thumb.onmouseup = null;
    };

};
Теперь никаких «прыжков» бегунка слайдера не происходит.

Кстати, в некоторых случаях уже начало срабатывать и «отпускание» бегунка (при отпускании основной кнопки мыши). Почему оно начало срабатывать? Как можно заметить из нашего кода (или потестировав наш слайдер на тестовой HTML-странице), «отпускание» бегунка в нашем случае срабатывает, только если мы отпускаем основную кнопку мыши над бегунком. Это происходит потому, что мы повесили обработчик «отпускания» объекта на событие мыши mouseup именно для HTML-элемента с идентификатором thumb. В самом начале бегунок из-за неправильного вычисления координат «отскакивал» из-под курсора мыши, и отпускание кнопки мыши происходило не над бегунком, поэтому «отпускания» бегунка и не происходило.

Хорошо, но теперь мне бы хотелось, чтобы «отпускание» бегунка слайдера происходило в любом случае, независимо от того, где в данный момент находится курсор мыши — над бегунком или не над ним.

Что для этого нужно сделать? Обратим внимание, что движение бегунка в нашем коде выполняется, как раз, именно что в любом случае, без разницы, находится ли курсор мыши над бегунком или не находится. Почему движение бегунка работает таким образом? Потому что мы повесили обработчик движения мыши на событие мыши mousemove для HTML-страницы (document), а не для HTML-элемента thumb. Сделаем то же самое для события мыши mouseup. Меняем код (я отметил изменения красным цветом):
let sliderRect = slider.getBoundingClientRect();

let thumb = document.querySelector(".thumb");
thumb.onmousedown = function(event) { // захват бегунка слайдера

    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.pageX - thumbRect.x;

    // функция, реализующая движение бегунка слайдера
    function onMouseMove(event) {
        let x = event.pageX - sliderRect.x - shiftX;
        thumb.style.left = x + "px";
    }

    document.addEventListener("mousemove", onMouseMove);

    // функция, реализующая отпускание бегунка слайдера
    function onMouseUp() {
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };

    document.addEventListener("mouseup", onMouseUp);
};

Теперь наш слайдер работает так, как следует. Единственное, что осталось исправить: бегунок можно задвинуть влево и вправо за пределы слайдера. Чтобы это предотвратить, достаточно внести дополнения в функцию onMouseMove (я отметил дополнения красным цветом):
    function onMouseMove(event) {
        let x = event.pageX - sliderRect.x - shiftX;
        if (x < 0) x = 0;
        if (x > slider.clientWidth - thumb.offsetWidth)
            x = slider.clientWidth - thumb.offsetWidth;
        thumb.style.left = x + "px";
    }

* * *

Я посмотрел решение от авторов задачи, оно практически идентично вышеприведенному за исключением двух деталей.

Во-первых, авторы задачи в своем скрипте в самом конце добавили следующий код:
thumb.ondragstart = function() {
    return false;
};

Смысл этого кода был объяснен в обсуждаемом подразделе учебника. Для некоторых случаев у браузера существует и автоматически включается умолчательный механизм «Drag’n’Drop». Вышеприведенный код нужен для отключения этого умолчательного механизма, чтобы он не вошел в конфликт с реализованным нами алгоритмом «Drag’n’Drop». Однако, этот умолчательный механизм включается не во всех случаях, а только для ссылок (HTML-элемент a), картинок (HTML-элемент img) и выделений (по-английски «selection»). Наш случай не подпадает ни под одну из этих трех категорий (мы переносим HTML-элемент div, представляющий бегунок слайдера), а, значит, этот код для события dragstart нам не требуется (но и не мешает). По следующей ссылке об этом можно прочесть подробнее:

https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations

Во-вторых, в начало функции-обработчика события мыши mousedown (захват объекта) авторы задачи вставили такой код:
event.preventDefault(); // предотвратить запуск выделения (действие браузера)

Я бы не рекомендовал в данном случае добавлять это в код: у нас нет ни текста, ни картинок, поэтому, по идее, автоматического выделения от браузера не будет.

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

Такое поведение возникает, только если мы работаем с нашим слайдером через встроенный кадр (HTML-элемент iframe) и при этом в функции-обработчике события мыши mousedown есть инструкция event.preventDefault();. Я пока не знаю, почему это происходит, но постараюсь выяснить (см. тут).
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments