Доброго времени суток, друзья!
В данном туториале мы рассмотрим встроенный механизм перетаскивания элементов на странице.
Справедливости ради следует отметить, что указанный механизм можно реализовать с помощью событий мыши, как показывает Илья Кантор в своем учебнике, однако мы будем использовать нативные средства, опираясь на спецификацию.
Поддержка технологии:
Превью:
Наша задача состоит в следующем: реализовать список задач, состоящий из трех колонок: все задачи, задачи, находящиеся в процессе выполнения, завершенные задачи. Разумеется, приложение должно предусматривать возможность добавления и удаления задач. Кроме того, должна быть предусмотрена возможность произвольного расположения задач. Это одна из наиболее интересных частей туториала — отслеживание элемента, находящегося под перетаскиваемым, и определение того, где должен располагаться перетаскиваемый элемент, над или под отслеживаемым.
Для стилизации будет использоваться Bootstrap.
Если вам это интересно, прошу следовать за мной.
Разметка:
Здесь у нас имеется контейнер с полем для ввода текста задачи и кнопкой для ее добавления в список (input-group), а также три контейнера-колонки (list-group) для всех задач (todos-list), задач в процессе выполнения (in-progress-list) и завершенных задач (completed-list). Что касается атрибутов «data», то они предназначены для разделения стилизации и управления: классы — для стилизации, data — для управления.
Стили:
Классы «in-progress» и «completed» служат индикаторами нахождения задачи в соответствующей колонке. Класс «drop» предназначен для визуализации попадания задачи в зону для «бросания».
Прежде чем переходить к скрипту, отметим, что нами будут использоваться далеко не все события перетаскивания, но большинство из основных.
Определяем главный контейнер, в котором будет осуществляться поиск элементов и которому будет делегирована обработка событий:
Реализуем добавление и удаление задач через обработку клика:
Переходим непосредственно к перетаскиванию.
Для начала реализуем попадание в зону для «бросание» и уход из нее посредством добавления/удаления соответствующего класса:
Далее обрабатываем начало перетаскивания:
Теперь нам нужно каким-то образом отслеживать элемент, находящийся под перетаскиваемым. Это необходимо для того, чтобы произвольно располагать задачи в списке, т.е. менять задачи в колонке местами. При обработке события «mousemove» для этого используется метод «elementFromPoint(x, y)». Прелесть рассматриваемого интерфейса состоит в том, что для определения «низлежащего» элемента нам достаточно обработать событие «dragover»:
Наконец, обрабатываем событие «drop» («бросание»):
Вот и все. Как видите, ничего сложного. Зато какие возможности по добавлению интерактивности на страницу. Осталось дождаться, когда мобильные браузеры реализуют данную технологию, и будет всем счастье.
Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание и хорошего дня.
В данном туториале мы рассмотрим встроенный механизм перетаскивания элементов на странице.
Справедливости ради следует отметить, что указанный механизм можно реализовать с помощью событий мыши, как показывает Илья Кантор в своем учебнике, однако мы будем использовать нативные средства, опираясь на спецификацию.
Поддержка технологии:
Превью:
Наша задача состоит в следующем: реализовать список задач, состоящий из трех колонок: все задачи, задачи, находящиеся в процессе выполнения, завершенные задачи. Разумеется, приложение должно предусматривать возможность добавления и удаления задач. Кроме того, должна быть предусмотрена возможность произвольного расположения задач. Это одна из наиболее интересных частей туториала — отслеживание элемента, находящегося под перетаскиваемым, и определение того, где должен располагаться перетаскиваемый элемент, над или под отслеживаемым.
Для стилизации будет использоваться 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: linear-gradient(#eee, transparent);
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";
}
}
});
Вот и все. Как видите, ничего сложного. Зато какие возможности по добавлению интерактивности на страницу. Осталось дождаться, когда мобильные браузеры реализуют данную технологию, и будет всем счастье.
Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание и хорошего дня.