Как аккуратно сделать несколько элементов перемещаемыми в любом месте?

Основываясь на предложениях здесь, если для перемещения требуется только один элемент, возможно, ниже приведен простой способ сделать это:

(function () {
    let offset = [0, 0];
    let target = document.querySelector('.target');
    let isDown = false;
    target.addEventListener('mousedown', function(e) {
        isDown = true;
        target.style.position = 'relative';
        offset = [
            target.offsetLeft - e.clientX,
            target.offsetTop - e.clientY
        ];
    }, true);

    document.addEventListener('mouseup', function() {
        isDown = false;
    }, true);

    document.addEventListener('mousemove', function(e) {
        event.preventDefault();
        if (isDown) {
            target.style.left = (e.clientX + offset[0]) + 'px';
            target.style.top = (e.clientY + offset[1]) + 'px';
        }
    }, true);
})();
 

.target {
    width: 100px;
    height: 100px;
    background-color: #0000FF;
}
 

<div class = "target"></div>

Что делать, если есть 2 div? а если 1000? тогда подход, описанный выше, будет не таким удобным, потому что, если у нас есть 1000 элементов div, нам нужно еще 1000 прослушивателей событий, отслеживающих 1000 offset, а также isDown переменных. Во время более ранней попытки я попытался избавиться от логики offset и isDown с помощью $('.target').offset() и сделать расчет внутри обработчика mousemove, но мне это не удалось. Вот попытка сделать то, что я описал ранее, и это то, что я пытаюсь улучшить:

(function() {
    let targetsData = {};
    let targets = document.querySelectorAll('.target');
    Array.prototype.map.call(targets, (target) => {
        targetsData[target] = { 'mousedown': false, 'offset': [0, 0] };
        target.addEventListener('mousedown', (e) => {
            target.style.position = 'relative';
            targetsData[target]['mousedown'] = true;
            targetsData[target]['offset'] = [
                target.offsetLeft - e.clientX,
                target.offsetTop - e.clientY
            ];
        });
        target.addEventListener('mouseup', (e) => {
            targetsData[target]['mousedown'] = false;
        });
        target.addEventListener('mousemove', (e) => {
            e.preventDefault();
            if (targetsData[target]['mousedown']) {
                let offset = targetsData[target]['offset']
                target.style.left = (e.clientX + offset[0]) + 'px';
                target.style.top = (e.clientY + offset[1]) + 'px';
            }
        });
    });
})();
 

.target {
    width: 100px;
    height: 100px;
    background-color: #0000FF;
    margin-bottom: 5px
}
 

<div class = "target"></div>
<div class = "target"></div>
<div class = "target"></div>

Работает только первый квадрат сверху, и если его быстро перетащить, начинают происходить какие-то странные эффекты, а побочные эффекты других кажутся хуже:

Что тут происходит? Как можно эффективно заставить все квадраты двигаться правильно?

Итак, здесь падает пара вещей. Вы не можете использовать свой объект target в качестве ключа поиска в простом объекте — он будет преобразован в строку (вместо этого см. Карта). Странные скачки для ваших второго и третьего элементов div связаны с тем, что вы добавляете их offset позиции от mousedown к их относительной позиции при mousemove, поэтому они будут постепенно перемещаться дальше влево и вниз.

motto 08.04.2023 23:29

Исходя из фона python, это работает в python, поэтому я предположил, что это будет с использованием простых объектов. Означает ли это, что при преобразовании в строку 3 будут иметь одну и ту же строку и, следовательно, на самом деле будет существовать только один ключ поиска? Что касается странных прыжков, почему они происходят только или сильнее в последних 2 клетках?

nlblack323 08.04.2023 23:37

Объект Map представляет собой более общий поиск в стиле dict Python, тогда как ключи объекта JS представляют собой строки, числа и символы, хотя, если вы используете что-либо еще в качестве ключа, он будет преобразован в строку; в этом случае строковое представление может зависеть от браузера, но похоже на [object HTMLDivElement] во всех трех случаях. Скачки случаются только сильнее с двумя последними квадратами, потому что начальные offsetTop и offsetLeft первого квадрата равны 0, поскольку он начинается в верхнем левом углу страницы.

motto 08.04.2023 23:43
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
3
68
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Есть много возможностей — на самом деле не так уж сложно иметь много обработчиков событий, хотя управление ими может быть проблематичным, если вы не используете фреймворк.

Довольно простой вариант — использовать один обработчик mousedown, зарегистрированный в документе, и проверить в обработчике событий, является ли цель события (или любой из его предков) одним из перетаскиваемых элементов:

Примечание: первоначальная версия не обрабатывала повторное перетаскивание корректно.

(function() {
    let offset = [0, 0];
    let target = null;
    
    function targetIfMatches(e, selector) {
        let maybeTarget = e.target;
        while (maybeTarget && maybeTarget.matches) {
            if (maybeTarget.matches(selector))
                return maybeTarget;
            maybeTarget = maybeTarget.parentNode;
        }
        return null;
    }

    function handleMouseDown(e) {
        // Check event target and ancestors to see if any of them are a target
        target = targetIfMatches(e, ".target");

        if (target) {
            target.style.position = 'relative';
            offset = [
                [e.pageX - parseFloat(target.style.left || 0)],
                [e.pageY - parseFloat(target.style.top || 0)]
            ];
        }
    }

    function handleMouseMove(e) {
        e.preventDefault();
        if (target) {
            target.style.left = (e.pageX - offset[0]) + 'px';
            target.style.top = (e.pageY - offset[1]) + 'px';
        }
    }
    
    function handleMouseUp(e) {
        target = null;
    }

    document.addEventListener('mousedown', handleMouseDown, true);
    document.addEventListener('mouseup', handleMouseUp, true);
    document.addEventListener('mousemove', handleMouseMove, true);
})();
.target {
  width: 100px;
  height: 100px;
  background-color: #0000FF;
  text-align: center;
  font: 20px/100px sans-serif;
  color: white;
  cursor: move;
  margin: 5px;
}
<div class = "target">1</div>
<div class = "target">2</div>
<div class = "target">3</div>
<div class = "target">4</div>
<div class = "target">5</div>
<div class = "target">6</div>

Это решение ломается после нескольких попыток переместить один div

Rojo 08.04.2023 22:22

После редактирования все квадраты двигаются, как и ожидалось, но таким образом 2 прослушивателя событий возрождаются при любом нажатии мыши. Хотя это не повлияет на производительность, это расточительно, менее читабельно и немного запутанно. Как вы думаете, есть ли более чистый способ добиться того же?

nlblack323 08.04.2023 23:47

Все три обработчика событий подключены при начальной настройке. Обработчики mouseup и mousemove не возрождаются на mousedown.

motto 08.04.2023 23:49

Ага, только что заметил, что их нет, наверное форматирование меня смутило.

nlblack323 08.04.2023 23:52

Прошу прощения, я использовал код из вашего фрагмента и чрезмерно усердную tidy функциональность, предоставляемую интерфейсом фрагмента.

motto 08.04.2023 23:54

Также есть необработанное исключение, возникающее при mousdowns, где нет цели. Uncaught TypeError: maybeTarget.matches is not a function at HTMLDocument.<anonymous> (...)

nlblack323 09.04.2023 00:02

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

motto 09.04.2023 00:08
Ответ принят как подходящий

Вам не нужен массив смещений. Глобальный будет работать нормально. В любом случае вы можете хранить отдельные значения внутри самого элемента. Это сэкономит немного памяти и времени. Мне также не нравится идея добавления смещения. Вместо этого вычтите смещение мыши из текущей позиции мыши. Это смещение будет равно смещению div от момента первой загрузки DOM плюс смещение позиции мыши от текущего положения div.

(function() {
  let targets = document.querySelectorAll('.target');
  let offsetX;
  let offsetY;
  Array.prototype.map.call(targets, (target) => {
    target.isMouseDown = false;
    target.initialOffsetLeft = target.offsetLeft;
    target.initialOffsetTop = target.offsetTop;
    target.addEventListener('mousedown', (e) => {
      target.style.position = 'relative';
      target.isMouseDown = true;
      offsetX = target.initialOffsetLeft + e.offsetX;
      offsetY = target.initialOffsetTop + e.offsetY;
    });
    document.addEventListener('mouseup', (e) => {
      target.isMouseDown = false;
    });
    document.addEventListener('mousemove', (e) => {
      e.preventDefault();
      if (target.isMouseDown) {
        target.style.left = e.pageX - offsetX + 'px';
        target.style.top = e.pageY - offsetY + 'px';
      }
    });
  });
})();
.target {
  width: 300px;
  height: 300px;
  background-color: #0000FF;
  margin-bottom: 5px;
}
<div class = "target"></div>
<div class = "target"></div>
<div class = "target"></div>

Одно из основных различий между нашими кодами заключается в том, где мы размещаем обработчики событий. Я размещаю свои mouseup и mousemove события на document, а не на самом элементе. Таким образом, не имеет значения, где в документе находится мышь. Ваш код требует, чтобы мышь находилась над div, чтобы обрабатывать движения мыши или mouseup. И тут разница в вылете. Сначала легкая часть, я использую event.pageXY вместо event.clientXY, чтобы учесть прокрутку. Затем я сохранил начальное смещение каждого div внутри самого div. Я добавляю начальное смещение к смещению мыши относительно div для каждого mousedown, чтобы установить offsetXY. Наконец, я вычитаю смещение мыши из положения мыши, чтобы установить положение div.

Примечание. Первоначально я обозначил offsetXY как initialXY. Я понял, что термин initial неверно представляет переменную. Просто изменение имен переменных.

третий квадрат имеет такие же странные побочные эффекты

nlblack323 08.04.2023 22:29

@ nlblack323 Да, это происходит при прокрутке. Будут ли ваши пользователи прокручивать? Я могу добавить поддержку прокрутки

Rojo 08.04.2023 23:00

Я собираюсь использовать его для настройки эффекта перетаскивания, потому что все существующие известные решения выглядят ужасно или обременительны для использования или импорта. Так что да, это должно работать при перетаскивании из любого места в любое место. И что не так с тем, что я сделал? почему он не работает должным образом?

nlblack323 08.04.2023 23:16

@ nlblack323 Я добавил поддержку прокрутки, используя pageX и pageY вместо clientXY. И, честно говоря, я не совсем уверен, что именно делает ваш код. Я посмотрю немного больше.

Rojo 09.04.2023 00:06

да, ваш фрагмент в настоящее время работает, как и ожидалось.

nlblack323 09.04.2023 00:17

@ nlblack323 Я добавил огромное объяснение. По сути, вам нужно сохранить смещение div при загрузке DOM и никогда не обновлять его. Ваша самая большая ошибка — обновлять его каждый раз.

Rojo 09.04.2023 00:26

Другие вопросы по теме