ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Categories:

JavaScript: расставить героев на поле, решение задачи

Начало: JavaScript: расставить героев на поле, постановка задачи.

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

Сначала, как и советуют авторы задачи, повесим одну функцию-обработчик на событие mousedown на документе (будем использовать делегирование обработки этого события от перетаскиваемых объектов к документу):
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;

    // ...
}

event.target — это HTML-элемент, над которым пользователь нажал кнопку мыши. Если этот HTML-элемент не содержит в своем списке классов CSS-класс draggable, то такой HTML-элемент мы в данной функции-обработчике обрабатывать не будем, а завершим работу функции (инструкция return;). То есть в данной функции будем обрабатывать только перетаскиваемые объекты (в данном случае — 6 объектов-героев и мяч).

Сразу избавимся от действий браузера по умолчанию, которые в данном случае могут помешать работе скрипта. Пока пользователь еще не может перетаскивать героев и мяч на тестовой HTML-странице. Но, если он, например, наведет курсор мыши на одного из шести героев, нажмет основную кнопку мыши, а затем, не отпуская кнопку мыши, станет перетаскивать курсор мыши вверх HTML-страницы, над текстом, который там находится, то текст начнет выделяться. Выделение текста — это действие браузера по умолчанию, которое запускается после события мыши mousedown.

Кроме этого, после события mousedown для HTML-элемента img запускается умолчательная обработка браузером перетаскивания HTML-элементов, что выражается в появлении полупрозрачной копии мяча (напомню, мяч в данном случае представлен HTML-элементом img) при попытке перетаскивания мяча мышкой.

Отключим эти два действия браузера по умолчанию (изменения в коде я выделил красным цветом):
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    // ...
}

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

Реализуем дальнейший механизм процесса перетаскивания («Drag’n’Drop») объектов-героев или мяча так, как мы это уже делали в задаче про слайдер (и близко к тому, как это было показано в подразделе 3.3 «Drag'n'Drop с событиями мыши» второй части обсуждаемого учебника). Дополним код:
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    // ...

    function onMouseMove(event) { // перетаскивание объекта
        // ...
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Теперь реализуем движение объекта при перетаскивании по аналогии с тем, как это было сделано в задаче про слайдер (и близко к тому, как это было показано в подразделе 3.3 «Drag'n'Drop с событиями мыши» второй части обсуждаемого учебника). Дополним код (изменения я отметил красным цветом):
Скрипт 1.
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

    function onMouseMove(event) { // перетаскивание объекта
        let x, y;
        x = event.pageX - shiftX;
        y = event.pageY - shiftY;

        tar.style.position = "absolute";
        tar.style.left = x + "px";
        tar.style.top = y + "px";
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Вместо бегунка слайдера теперь — перетаскиваемый объект tar. И мы передвигаем объект не только по горизонтали, как в случае с бегунком слайдера, а и по вертикали тоже (то есть могут меняться и координата x, и координата y).

Стало не нужно учитывать сдвиг слайдера, на котором двигался бегунок, так как наш перетаскиваемый объект может двигаться по всей области просмотра браузера без ограничений. Это немного упростило формулу для вычисления координат x и y.

Позиционирование бегунка слайдера было относительным (он позиционировался относительно верхнего левого угла клиентской части своего контейнера, то есть слайдера). Для перетаскиваемых объектов я решил выбрать абсолютное позиционирование (координаты перетаскиваемого объекта вычисляются относительно левого верхнего угла тестовой HTML-страницы, поэтому берем event.pageX и event.pageY).

И тут выяснилась одна интересная деталь, которую я упустил при чтении подраздела 3.3 «Drag'n'Drop с событиями мыши» второй части обсуждаемого учебника (там она была затронута). В задаче со слайдером я использовал для вычисления сдвига курсора мыши относительно левого верхнего угла перетаскиваемого объекта такой код:
    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.pageX - thumbRect.x;

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

Для прокручиваемой HTML-страницы правильно было бы учесть возможное смещение HTML-страницы по вертикали или горизонтали. Например, так:
    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.pageX - pageXOffset - thumbRect.x;
Ну а это то же самое, что и
    let thumbRect = thumb.getBoundingClientRect();
    let shiftX = event.clientX - thumbRect.x;
Что для нашей теперешней задачи с героями, мячом и футбольным полем превращается в
    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

В принципе, вышеприведенный скрипт 1 уже обеспечивает пользователю возможность перетаскивать героев и мяч по тестовой HTML-странице, оставлять их в любом месте, в частности, на футбольном поле.

Отмечу тут интересную особенность: при начале перемещения любого из шести перетаскиваемых объектов-героев он исключается из нормального потока HTML-страницы (потому что мы включаем для него в скрипте абсолютное позиционирование), его место освобождается, но другие объекты-герои, идущие в коде HTML-страницы за ним, сразу же автоматически сдвигаются на его место. Как я понимаю, это происходит из-за того, что в стиле этих перетаскиваемых объектов есть указание float: left;.

Но мы не можем остановиться на скрипте 1, так как задача ставит еще две дополнительные подзадачи: во-первых, скрипт не должен позволять перетащить объект так, чтобы его часть вышла за границы области просмотра браузера (скрипт 1 это позволяет); во-вторых, при перетаскивании объекта, если край объекта достиг края области просмотра браузера, окно браузера должно прокрутиться в нужную сторону, чтобы позволить дальнейшее перетаскивание объекта в желаемую пользователем сторону (скрипт 1 этого не умеет).

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

0 ⩽ x ⩽ (ширина области просмотра браузера – ширина перетаскиваемого объекта)
0 ⩽ y ⩽ (высота области просмотра браузера – высота перетаскиваемого объекта)

или в терминах нашего кода на языке JavaScript:

0 ⩽ xdocument.documentElement.clientWidth - tar.offsetWidth
0 ⩽ ydocument.documentElement.clientHeight - tar.offsetHeight

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

pageXOffsetxdocument.documentElement.clientWidth + pageXOffset - tar.offsetWidth
pageYOffsetydocument.documentElement.clientHeight + pageYOffset - tar.offsetHeight

Дополним код (я отметил изменения красным цветом):
Скрипт 2.
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

    function onMouseMove(event) { // перетаскивание объекта
        let x, y;
        x = event.pageX - shiftX;
        y = event.pageY - shiftY;

        if (x > document.documentElement.clientWidth + pageXOffset - tar.offsetWidth)
            x = document.documentElement.clientWidth + pageXOffset - tar.offsetWidth;
        if (x < pageXOffset) x = pageXOffset;

        if (y > document.documentElement.clientHeight + pageYOffset - tar.offsetHeight)
            y = document.documentElement.clientHeight + pageYOffset - tar.offsetHeight;
        if (y < pageYOffset) y = pageYOffset;

        tar.style.position = "absolute";
        tar.style.left = x + "px";
        tar.style.top = y + "px";
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

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

Реализуем последнюю подзадачу: обеспечение прокрутки HTML-страницы, если пользователь подносит перетаскиваемый объект к краю области просмотра браузера. Вообще в задаче поставлено условие обеспечить эту возможность только по вертикали, но мы сделаем и по горизонтали тоже (к чему нам полумеры?).

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

Делать это будем до каждого из соответствующих ограничений диапазонов, которые мы прописали в скрипте 2. Зачем нужно оставить ограничения диапазонов? Если их убрать, то у пользователя появится возможность прокручивать HTML-страницу в любом направлении бесконечно. Нам этого, думаю, не нужно (по крайней мере, эти ограничения оставлены в решении авторов задачи, я это заметил, наблюдая работу их решения на демонстрационной странице).

Почему ограничения диапазонов координат будут работать правильно, ведь мы их создали для области просмотра браузера? Дело в том, что перед проверкой каждого из ограничений мы сделаем соответствующую прокрутку HTML-страницы, как это было описано выше. Из-за этого границы диапазонов изменятся и позволят двигать объект в нужном направлении.

Как тогда ограничивается бесконечная прокрутка в любом направлении? Метод scrollBy срабатывает только до тех пор, пока в требуемом направлении за границей области просмотра еще имеется скрытый кусок HTML-страницы. Как только в требуемом направлении HTML-страница будет прокручена до своего конца, метод scrollBy перестанет срабатывать. (В случае без ограничения диапазона передвижение объекта за границу области просмотра и одновременно за границу HTML-страницы дает место для срабатывания метода scrollBy. По крайней мере, так это работает в моем браузере.)

Дополняем код (я отметил красным цветом изменения):
Скрипт 3.
document.addEventListener("mousedown", onMouseDown);
function onMouseDown(event) {     // захват объекта
    let tar = event.target;
    if (!tar.classList.contains("draggable")) return;
    event.preventDefault();

    let tarRect = tar.getBoundingClientRect();
    let shiftX = event.clientX - tarRect.x;
    let shiftY = event.clientY - tarRect.y;

    function onMouseMove(event) { // перетаскивание объекта
        let x, y;
        x = event.pageX - shiftX;
        y = event.pageY - shiftY;

        if (x > document.documentElement.clientWidth + pageXOffset - tar.offsetWidth)
            scrollBy(x - (document.documentElement.clientWidth + pageXOffset - tar.offsetWidth), 0);
        if (x > document.documentElement.clientWidth + pageXOffset - tar.offsetWidth)
            x = document.documentElement.clientWidth + pageXOffset - tar.offsetWidth;

        if (x < pageXOffset) scrollBy(x - pageXOffset, 0);
        if (x < pageXOffset) x = pageXOffset;

        if (y > document.documentElement.clientHeight + pageYOffset - tar.offsetHeight)
            scrollBy(0, y - (document.documentElement.clientHeight + pageYOffset - tar.offsetHeight));
        if (y > document.documentElement.clientHeight + pageYOffset - tar.offsetHeight)
            y = document.documentElement.clientHeight + pageYOffset - tar.offsetHeight;

        if (y < pageYOffset) scrollBy(0, y - pageYOffset);
        if (y < pageYOffset) y = pageYOffset;

        tar.style.position = "absolute";
        tar.style.left = x + "px";
        tar.style.top = y + "px";
    }

    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    function onMouseUp() {        // отпускание объекта
        document.removeEventListener("mousemove", onMouseMove);
        document.removeEventListener("mouseup", onMouseUp);
    };
}

Это окончательный вариант скрипта.
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments