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

Превью:
Наша задача состоит в следующем: реализовать список задач, состоящий из трех колонок: все задачи, задачи, находящиеся в процессе выполнения, завершенные задачи. Разумеется, приложение должно предусматривать возможность добавления и удаления задач. Кроме того, должна быть предусмотрена возможность произвольного расположения задач. Это одна из наиболее интересных частей туториала — отслеживание элемента, находящегося под перетаскиваемым, и определение того, где должен располагаться перетаскиваемый элемент, над или под отслеживаемым.
Для стилизации будет использоваться 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/
Добавить комментарий