Drag’n’Drop API: пример использования

от автора

Доброго времени суток, друзья!

В данном туториале мы рассмотрим встроенный механизм перетаскивания элементов на странице.

Справедливости ради следует отметить, что указанный механизм можно реализовать с помощью событий мыши, как показывает Илья Кантор в своем учебнике, однако мы будем использовать нативные средства, опираясь на спецификацию.

Поддержка технологии:

Превью:

Наша задача состоит в следующем: реализовать список задач, состоящий из трех колонок: все задачи, задачи, находящиеся в процессе выполнения, завершенные задачи. Разумеется, приложение должно предусматривать возможность добавления и удаления задач. Кроме того, должна быть предусмотрена возможность произвольного расположения задач. Это одна из наиболее интересных частей туториала — отслеживание элемента, находящегося под перетаскиваемым, и определение того, где должен располагаться перетаскиваемый элемент, над или под отслеживаемым.

Для стилизации будет использоваться Bootstrap.

Если вам это интересно, прошу следовать за мной.

Разметка:

<head>     <!-- Bootstrap CSS -->     <link       rel="stylesheet"       href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"       integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z"       crossorigin="anonymous"     />     <!-- custom CSS -->     <link rel="stylesheet" href="style.css" />   </head>   <body class="container">     <h1>Drag & Drop Example</h1>     <main class="row">       <div class="input-group">         <div class="input-group-prepend">           <span class="input-group-text">Enter new todo: </span>         </div>         <input           type="text"           class="form-control"           placeholder="todo4"           data-name="todo-input"         />         <div class="input-group-append">           <button class="btn btn-success" data-name="add-btn">Add</button>         </div>       </div>        <div class="col-4">         <h3>Todos</h3>         <ul class="list-group" data-name="todos-list">           <li class="list-group-item" data-id="1" draggable="true">             <p>todo1</p>             <button               class="btn btn-outline-danger btn-sm"               data-name="remove-btn"             >               X             </button>           </li>           <li class="list-group-item" data-id="2" draggable="true">             <p>todo2</p>             <button               class="btn btn-outline-danger btn-sm"               data-name="remove-btn"             >               X             </button>           </li>           <li class="list-group-item" data-id="3" draggable="true">             <p>todo3</p>             <button               class="btn btn-outline-danger btn-sm"               data-name="remove-btn"             >               X             </button>           </li>         </ul>       </div>        <div class="col-4">         <h3>In Progress</h3>         <ul class="list-group" data-name="in-progress-list"></ul>       </div>        <div class="col-4">         <h3>Completed</h3>         <ul class="list-group" data-name="completed-list"></ul>       </div>     </main>      <!-- custom JS -->     <script src="script.js"></script> </body> 

Здесь у нас имеется контейнер с полем для ввода текста задачи и кнопкой для ее добавления в список (input-group), а также три контейнера-колонки (list-group) для всех задач (todos-list), задач в процессе выполнения (in-progress-list) и завершенных задач (completed-list). Что касается атрибутов «data», то они предназначены для разделения стилизации и управления: классы — для стилизации, data — для управления.

Стили:

body {   min-height: 100vh;   display: flex;   flex-direction: column;   justify-content: center;   align-items: center;   color: #222; }  main {   max-width: 600px; }  .input-group {   margin: 1rem; }  .list-group {   min-height: 100px;   height: 100%; }  .list-group-item {   display: flex;   justify-content: space-between;   align-items: center; }  div + div {   border-right: 1px dotted #222; }  h3 {   text-align: center; }  p {   margin: 0; }  .completed p {   text-decoration: line-through; }  .in-progress p {   border-bottom: 1px dashed #222; }  .drop {   background: #eee;   border-radius: 4px; }  

Классы «in-progress» и «completed» служат индикаторами нахождения задачи в соответствующей колонке. Класс «drop» предназначен для визуализации попадания задачи в зону для «бросания».

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

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

const main = document.querySelector("main"); 

Реализуем добавление и удаление задач через обработку клика:

main.addEventListener("click", (e) => {   // нас интересует только нажатие кнопки   if (e.target.tagName === "BUTTON") {     // получаем название кнопки из атрибута "data-name"     const { name } = e.target.dataset;     // если перед нами кнопка для добавления задачи в список     if (name === "add-btn") {       // определяем поле для ввода текста задачи       const todoInput = main.querySelector('[data-name="todo-input"]');       // если оно не является пустым       if (todoInput.value.trim() !== "") {         // получаем текст задачи         const value = todoInput.value;         // создаем шаблон задачи         const template = `         <li class="list-group-item" draggable="true" data-id="${Date.now()}">           <p>${value}</p>           <button class="btn btn-outline-danger btn-sm" data-name="remove-btn">X</button>         </li>         `;         // находим список задач         const todosList = main.querySelector('[data-name="todos-list"]');         // добавляем в него шаблон задачи         todosList.insertAdjacentHTML("beforeend", template);         // очищаем поле для ввода текста задачи         todoInput.value = "";       }     // если перед нами кнопка для удаления задачи     } else if (name === "remove-btn") {       // просто удаляем ее       e.target.parentElement.remove();     }   } }); 

Переходим непосредственно к перетаскиванию.

Для начала реализуем попадание в зону для «бросание» и уход из нее посредством добавления/удаления соответствующего класса:

main.addEventListener("dragenter", (e) => {   // нас интересуют только колонки   if (e.target.classList.contains("list-group")) {     e.target.classList.add("drop");   } });  main.addEventListener("dragleave", (e) => {   if (e.target.classList.contains("drop")) {     e.target.classList.remove("drop");   } }); 

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

main.addEventListener("dragstart", (e) => {   // нас интересует только задача   if (e.target.classList.contains("list-group-item")) {     // сохраняем идентификатор задачи в объекте "dataTransfer" в виде обычного текста;     // dataTransfer также позволяет сохранять HTML - text/html,     // но в данном случае нам это ни к чему     e.dataTransfer.setData("text/plain", e.target.dataset.id);   } }); 

Теперь нам нужно каким-то образом отслеживать элемент, находящийся под перетаскиваемым. Это необходимо для того, чтобы произвольно располагать задачи в списке, т.е. менять задачи в колонке местами. При обработке события «mousemove» для этого используется метод «elementFromPoint(x, y)». Прелесть рассматриваемого интерфейса состоит в том, что для определения «низлежащего» элемента нам достаточно обработать событие «dragover»:

// создаем переменную для хранения "низлежащего" элемента let elemBelow = "";  main.addEventListener("dragover", (e) => {   // отключаем стандартное поведение браузера;   // это необходимо сделать в любом случае   e.preventDefault();    // записываем в переменную целевой элемент;   // валидацию сделаем позже   elemBelow = e.target; }); 

Наконец, обрабатываем событие «drop» («бросание»):

main.addEventListener("drop", (e) => {   // находим перетаскиваемую задачу по идентификатору, записанному в dataTransfer   const todo = main.querySelector(     `[data-id="${e.dataTransfer.getData("text/plain")}"]`   );    // прекращаем выполнение кода, если задача и элемент - одно и тоже   if (elemBelow === todo) {     return;   }    // если элементом является параграф или кнопка, значит, нам нужен их родительский элемент   if (elemBelow.tagName === "P" || elemBelow.tagName === "BUTTON") {     elemBelow = elemBelow.parentElement;   }    // на всякий случай еще раз проверяем, что имеем дело с задачей   if (elemBelow.classList.contains("list-group-item")) {     // нам нужно понять, куда помещать перетаскиваемый элемент:     // до или после низлежащего;     // для этого необходимо определить центр низлежащего элемента     // и положение курсора относительно этого центра (выше или ниже)     // определяем центр     const center =       elemBelow.getBoundingClientRect().y +       elemBelow.getBoundingClientRect().height / 2;     // если курсор находится ниже центра     // значит, перетаскиваемый элемент должен быть помещен под низлежащим     // иначе, перед ним     if (e.clientY > center) {       if (elemBelow.nextElementSibling !== null) {         elemBelow = elemBelow.nextElementSibling;       } else {         return;       }     }      elemBelow.parentElement.insertBefore(todo, elemBelow);     // рокировка элементов может происходить в разных колонках     // необходимо убедиться, что задачи будут визуально идентичными     todo.className = elemBelow.className;   }    // если целью является колонка   if (e.target.classList.contains("list-group")) {     // просто добавляем в нее перетаскиваемый элемент     // это приведет к автоматическому удалению элемента из "родной" колонки     e.target.append(todo);      // удаляем индикатор зоны для "бросания"     if (e.target.classList.contains("drop")) {       e.target.classList.remove("drop");     }      // визуальное оформление задачи в зависимости от колонки, в которой она находится     const { name } = e.target.dataset;      if (name === "completed-list") {       if (todo.classList.contains("in-progress")) {         todo.classList.remove("in-progress");       }       todo.classList.add("completed");     } else if (name === "in-progress-list") {       if (todo.classList.contains("completed")) {         todo.classList.remove("completed");       }       todo.classList.add("in-progress");     } else {       todo.className = "list-group-item";     }   } }); 

Вот и все. Как видите, ничего сложного. Зато какие возможности по добавлению интерактивности на страницу. Осталось дождаться, когда мобильные браузеры реализуют данную технологию, и будет всем счастье.

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание и хорошего дня.

ссылка на оригинал статьи https://habr.com/ru/post/521394/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *