Создаем калькулятор-конвертер на базе React. Часть 7: Drag & Drop

Обсудить
Делаем калькулятор-конвертер на базе React. Часть 7: Drag & Drop

На этом этапе добавим в наш калькулятор интерфейс для перетаскивания элементов с помощью мыши. 

Предыдущий материал: Калькулятор-конвертер на базе React. Часть 6: Добавляем конвертер

Зачем нужен Drag&Drop

Вы наверняка уже видели подобные интерфейсы как на компьютере, так и на мобильных устройствах. Идея заключается в «физическом» взаимодействии с объектом, когда вы используете курсор, чтобы перенести элемент из одной части приложения в другую. Это довольно популярный вариант управления программой, так как с помощью Drag&Drop можно многие задачи сделать легче в исполнении, очевиднее для поиска и логичнее с точки зрения пользовательского опыта. А иногда такой способ управления становится чуть ли не единственным для реализации некоторых интересных функциональных решений. 

Мы будем вдохновляться приложением Calzy, где Drag&Drop, помимо перемещения результатов расчета в Memory Zone, используется также для быстрых подсчетов с перетаскиванием решенных примеров из истории расчетов в текущий пример. В Calzy это работает так:

  • Вы берете какой-то элемент из истории расчетов.

  • Тащите его поверх одного из выражений (+, -, /, *).

  • Отпускаете и получаете результата на основании выбранного выражения.

Мы будем внедрять в наше приложение аналогичную логику. 

Сам материал будет полезен даже тем, кто не делает с нами калькулятор, а просто ищет способ внедрения Drag&Drop. 

Комьюнити теперь в Телеграм
Подпишитесь и будьте в курсе последних IT-новостей
Подписаться

Создаем компонент для управления элементами

Для реализации даже базовой функциональности Drag&Drop нам придется написать довольно большое количество кода, а это наталкивает на мысль о том, что стоило бы перенести эту логику в отдельный файл. Как вариант, можно было бы создать отдельный элемент с названием dragging.js и импортировать из него функцию для управления данными. Но в таком случае мы бы потеряли контроль над частью данных внутри компонентов, так как они генерируются и хранятся в замкнутом виде, и большую их часть не удалось бы отыскать. Поэтому мы будем использовать компонент, чтобы можно было сделать запрос и к данным внутри приложения, и к применяемым методам, позволяющим обновлять информацию в интерфейсе программы. 

Файл с компонентом Dragging

Поэтому первым шагом станет создание нового файла в директории src. Назовем его Dragging.jsx

Добавляем draggable-элементы

Логика нашего Drag&Drop-интерфейса будет построена вокруг трех столпов. 

  1. Компонент Dragging, который содержит в себе функции, отвечающие за перемещение мыши. 

  2. Элементы с классом draggable, которые как раз и будут подвержены перемещению при помощи курсора. 

  3. Элементы с классом droppable. Те самые, что будут реагировать на «падение» на них других блоков данных.

Поэтому перед тем, как начать конструировать блок Dragging и добавлять основные функции взаимодействия с Drag&Drop-логикой, надо пометить нужные элементы в коде соответствующими классами. 

Начнем с Draggable. По нашей задумке в приложении должна быть возможность с помощью курсора перетягивать записи из истории подсчетов, то есть кнопки, формируемые в блоке History. Нужно открыть компонент History и в переменную results, где генерируются кнопки, добавить класс draggable. 

<Button className = 'draggable'> </Button>

Класс draggable у кнопок в блоке History

Этого достаточно. Главное, не перепутайте отдельные кнопки со всем блоком History.

Добавляем droppable-элементы

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

По тому же принципу находим компонент, где отрисовываются нужные нам кнопки, и прописываем className. 

<Button className="droppable"> </Button>

Класс droppable у кнопок в CountButton

Теперь у нас есть аж два из трех фрагментов, необходимых для реализации Drag&Drop. 

Реализуем компонент Dragging

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

Объективно – это не самый удобный интерфейс с точки зрения реализации, так как он довольно узконаправленный, и без трудозатрат перенести его в реалии более комплексного приложения не получится. Тем не менее в нашем случае он работает более чем исправно, поэтому мы будем придерживаться такой логики, не углубляясь в более специфичные и сложные структуры в духе сторонних библиотек от Atlassian (React Beautiful DnD) и других разработчиков. 

Сначала в файле Dragging.jsx создадим функцию Dragging и опишем в ней два первичных метода. 

Функция Dragging

  • Объявляем Dragging: 

    function Dragging(props) { }
  • В теле Dragging создаем переменную elements для хранения всех draggable. Так как draggable-элементы находятся в другом файле и недоступны внутри Dragging, нам нужно найти их при помощи классического перебора querySelectorAll и из полученного списка HTML-ветвей сделать массив (чтобы можно было работать с ним дальше):

    const elements = Array.from(document.querySelectorAll('.draggable'))
  • Перебираем список draggable и для каждого из них создаем eventListener, который будет активировать функцию drag при наведении мыши. 

    elements.map(e => e.addEventListener('mouseenter', function(e) { drag(e) }))

Описанный выше процесс необходим, так как применение функции drag (она отвечает за перемещение объекта) на всех draggable-кнопках заставит ползать по интерфейсу весь блок History, а нам это не нужно. С помощью addEventListener мы гарантируем себе, что Drag&Drop сработает только на одном элементе – на том, на который мы заранее навели мышь, и точно не на его соседях (об этом позаботится код внутри drag).

Но здесь возникает еще одна проблема. Дело в том, что до появления первой кнопки в блоке History, метод querySelectorAll не сможет найти ни одного draggable. Чтобы этого избежать, нужно скрыть перебор HTML-элементов в блок useEffect. 

Метод useEffect в функции Dragging

  • Сначала импортируем в файл Dragging.jsx хук useEffect: 

    import { useEffect } from 'react'
  • Затем добавляем метод useEffect в функцию Dragging и упаковываем в него прописанные ранее фрагменты кода. 

useEffect(() => { 
 const elements = Array.from(document.querySelectorAll('.draggable'))
 elements.map(e => e.addEventListener('mouse enter', function(e) {
 drag(e)
 }))
})

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

return ( <Box> {props.children} </Box> )

Элемент props.children

И не забываем экспортировать компонент Dragging, чтобы к нему был доступ извне. 

export default Dragging

Экспорт компонента Dragging

Реализуем контроль над перемещением мыши

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

Для этого создадим функцию drag. Она будет создавать для нас элемент, управляемый перетягиванием. А далее будет срабатывать метод onmousedown, регистрирующий зажатие левой клавиши мыши и меняющий свойства элемента draggable так, чтобы он мог свободно перемещаться по экрану. 

Функция drag

  • Объявляем внутри компонента Dragging функцию drag. 

    function drag(element) { }
  • В теле функции создаем переменную draggable и приравниваем ее к значению target у аргумента drag. 

    let draggable = element.target
  • Затем подключаем метод onmousedown и создаем внутри анонимную функцию для изменения положения кнопки с классом draggable. 

    draggable.onmousedown = function(event) { }
  • В теле метода onmousedown меняем свойства draggable. 

  • Указываем положение absolute, чтобы элемент не «спотыкался» о своих соседей: 

    draggable.style.position = 'absolute'
  • Меняем z-index на 1000, чтобы элемент точно отображался поверх всех остальных частей интерфейса. 

    draggable.style.zIndex = 1000
  • Прикрепляем draggable к body для свободного перемещения внутри приложения.

    document.body.append(draggable)
  • Внедряем метод moveAt, чтобы сместить объект. 

    moveAt(event.pageX, event.pageY)
  • Объявляем функцию moveAt, чтобы отслеживать координаты курсора. 

    function moveAt(pageX, pageY) { }
  • В теле moveAt создаем переменные, хранящие информацию о текущем положение элемента.

Готово. Теперь мы можем мышью таскать по экрану кнопки из History, но свою задачу выполнять они не смогут. Нужно настроить взаимодействие с элементами Droppable. 

Реализуем реакцию на попадание элемента в зону Droppable

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

Вот как выглядит код. Разберем его построчно. 

Функция onMouseMove

  • Объявляем переменную currentDroppable. По умолчанию она будет равна null. 

    let currentDroppable = null
  • Объявляем метод onMouseMove: 

    function onMouseMove(event) { }
  • В теле метода регистрируем движения мыши и двигаем вместе с ней объект: 

    moveAt(event.pageX, event.pageY)
  • Скрываем draggable:

    draggabe.hidden = true
  • В этот момент создаем элемент под сдвигаемой кнопкой: 

    let elemBelow = document.elementFromPoint(event.clientX, event.clientY)
  • Снова показываем draggable: 

    draggable.hidden = false
  • Делаем проверку на наличие искомого элемента под кнопкой: 

    if (!elementBelow) return
  • Если элемент найден и функция не прервалась, то создается переменная со свойствами фрагмента droppable под курсором: 

    let droppableBelow = elemBelow.closest('.droppable')
  • Потом проверяем совпадение между currentDroppable и droppableBelow: 

    if (currentDroppable != droppableBelow) { }
  • Внутри делаем еще одну проверку c запуском метода leaveDroppable: 

    if (currentDroppable) { leaveDroppable(currentDroppable) }
  • Переназначим переменную currentDroppable:

    currentDroppable = droppableBelow
  • Потом повторяем проверку с методом enterDroppable: 

    if (currentDroppable) { enterDroppable(currentDroppable) }

Описываем поведение мыши при наведении на разные части интерфейса

Мышь двигается. Объект под ней тоже. Осталось добавить сему действу смысла. 

Мы использовали выше два метода: enterDroppable и leaveDroppable. Думаю, по названию понятно, что один отвечает за поведение программы, когда элемент наведен на Droppable, а второй – наоборот. 

Я добавил в enterDroppable следующую логику:

Функция enterDroppable

  • Внутри создал функцию для метода onmouseup. Она срабатывает, когда сочетаются два сценария – курсор стоит над droppable и пользователь отпускает кнопку мыши. 

    draggable.onmouseup = function() { }
  • В методе onmouseup удаляю перетаскиваемую кнопку: 

    draggable.style.display = 'none'
  • Потом активирую функцию изменения результата. Она как раз производит сложение значения в перетаскиваемой кнопке со значением выражения, на которое мы скидываем кнопку. 

    props.setResult(eval(`${props.result} ${elem.innerHTML} ${draggable.innerHTML}`))
  • Удаляем listener, чтобы никто больше не следил за перемещением курсора. 

    document.removeEventListener('mouseove', onMouseMove);
  • «Обнуляем» значения draggable и прерываем функцию: 

    draggable = null; return

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

draggable.style.cursor = copy

Пусть вас не смущает название. Это общепринятый стандарт. 

Далее предусматриваем поведение при покидании зоны Droppable. Я просто возвращаю курсор в его привычный облик. 

Функция leaveDroppable

Отдельно стоит предусмотреть поведение объекта и курсора, если человек отпустит кнопку мыши где-то мимо Droppable. Я сделал так:

Функция onmouseup

  • После подключения addEventListener обращаюсь к методу onmouseup: 

    draggable.onmouseup = function() { }
  • В теле вновь созданной функции снова заставляю перетаскиваемый элемент пропасть: 

    draggable.style.display = 'none'
  • Удаляю addEventListener, отвечающий за отслеживание перемещение мыши. 

  • Использую метод setHistory, чтобы «вернуть» на место брошенный блок с решенным примером из истории. 

    props.setHistory(draggable.innerHTML)
  • И обнуляю значения draggable. 

    draggable = null

Подключаем Dragging-компонент к нашему приложению

Теперь необходимо обернуть наш ClickCalc в Dragging, чтобы описанный выше код заработал. 

Компонент Dragging, оборачивающий интерфейс ClickCalc

Обернув интерфейс калькулятора в Dragging, в качестве пропсов передаем в него текущий результат вычислений, метод обновления переменной result и метод обновления переменной history из Calculator.

В конце посоветую лишь внести небольшие визуальные изменения в блок History, задав ему фиксированную высоту, чтобы он не «прыгал» при вытаскивании из него кнопок. 

Метод return в блоке History

Вместо заключения

Получилось громоздко, но это рабочая реализация, отлично справляющаяся со своей основной задачей. Теперь у нас есть полноценно работающий Drag&Drop, и вы можете легко расширить его функциональность или применить в других приложениях.

Продолжение: Калькулятор-конвертер на базе React. Часть 8: Конвертер валют и собственный парсер данных

Hello World! Гайды и обзоры для девелоперов разных мастей.

Комментарии

Ваш баланс 10 ТК
1 ТК = 1 ₽
О том, как заработать и потратить Таймкарму, читайте в этой статье
Чтобы потратить Таймкарму, зарегистрируйтесь на нашем сайте
С помощью соцсетей
У меня нет аккаунта Зарегистрироваться
Нажимая кнопку «Зарегистрироваться», я даю согласие на обработку своих персональных данных, указанных в форме регистрации.
С помощью соцсетей
У меня уже есть аккаунт Войти
Нажимая кнопку «Зарегистрироваться», я даю согласие на обработку своих персональных данных, указанных в форме регистрации.
Инструкции по восстановлению пароля высланы на Ваш адрес электронной почты.
Пожалуйста, укажите email вашего аккаунта
Войти в Комьюнити
Регистрация в Комьюнити
Восстановление пароля