[JavaScript, Программирование, Разработка веб-сайтов] Drag'n'Drop API: пример использования

Автор Сообщение
news_bot ®

Стаж: 6 лет 2 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
30-Сен-2020 17:31


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

Превью:


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

Вот и все. Как видите, ничего сложного. Зато какие возможности по добавлению интерактивности на страницу. Осталось дождаться, когда мобильные браузеры реализуют данную технологию, и будет всем счастье.
Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание и хорошего дня.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_programmirovanie (Программирование), #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_draganddrop, #_drag_and_drop, #_drag&drop, #_peretaskivanie (перетаскивание), #_brosanie (бросание), #_javascript, #_programmirovanie (
Программирование
)
, #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 02-Май 13:17
Часовой пояс: UTC + 5