В прошлый раз мы начали делать свой Трелло-планировщик: создали страницу, заполнили тестовыми задачами и добавили стили, чтобы всё это выглядело красиво. Сегодня сделаем следующий важный шаг: добавим перетаскивание карточек и добавление новых задач. Очищение последней колонки тоже добавим, чтобы не делать это вручную по каждой карточке.
Логика проекта
Чтобы реализовать перетаскивание карточек, используем библиотеку Dragula JS. Её сделали специально для того, чтобы можно было легко подключить перетаскивание объектов на странице без прописывания сложной логики в коде. Если упростить, то все работы по перетаскиванию объектов берёт на себя библиотека, а мы лишь указываем, между какими колонками можно разрешить перетаскивание.
Общая логика работы сегодня будет такой:
- Подключаем библиотеку Dragula JS к странице.
- Добавляем действия к кнопкам, чтобы можно было добавлять и удалять карточки.
- Пишем скрипт для добавления и удаления.
- Добавляем в скрипт код для управления перетаскиванием.
- Настраиваем стили, чтобы всё выглядело хорошо.
Чтобы было проще, вот готовый код из прошлого проекта — эти файлы будем сегодня дорабатывать:
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Канбан-органайзер для задач</title>
<!-- подключаем стили -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- заголовок -->
<h1>Мои задачи</h1>
<!-- блок создания новой задачи -->
<div class="add-task-container">
<!-- поле ввода текста -->
<input type="text" maxlength="12" id="taskText" placeholder="Новая задача">
<!-- кнопка создания -->
<button id="add" class="button add-button">Добавить</button>
</div>
<!-- блок с задачами -->
<div class="main-container">
<!-- колонки -->
<ul class="columns">
<!-- первая колонка -->
<li class="column to-do-column">
<div class="column-header">
<h4>🌟 Сделать</h4>
</div>
<!-- задачи в колонке -->
<ul class="task-list" id="to-do">
<li class="task">
<p>Убрать на столе</p>
</li>
<li class="task">
<p>Приготовить обед</p>
</li>
<li class="task">
<p>Почитать</p>
</li>
<li class="task">
<p>Выбрать фильм</p>
</li>
</ul>
</li>
<!-- вторая колонка -->
<li class="column doing-column">
<div class="column-header">
<h4>💫 В работе</h4>
</div>
<ul class="task-list" id="doing">
<li class="task">
<p>Сделать презентацию</p>
</li>
<li class="task">
<p>Попить чай</p>
</li>
<li class="task">
<p>Забрать посылку</p>
</li>
</ul>
</li>
<!-- третья колонка -->
<li class="column done-column">
<div class="column-header">
<h4>🏆 Готово</h4>
</div>
<ul class="task-list" id="done">
<li class="task">
<p>Поспать</p>
</li>
<li class="task">
<p>Сделать зарядку</p>
</li>
</ul>
</li>
<!-- четвёртая колонка -->
<li class="column trash-column">
<div class="column-header">
<h4>❌ Не сделано</h4>
</div>
<ul class="task-list" id="trash">
<li class="task">
<p>Разобрать почту</p>
</li>
<li class="task">
<p>Купить телефон</p>
</li>
</ul>
<!-- кнопка очистки того, что не сделано -->
<div class="column-button">
<button class="button delete-button">Очистить</button>
</div>
</li>
</ul>
</div>
</body>
</html>
* {
/* делаем блоки фиксированной ширины во всех элементах */
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
/* общие настройки страницы */
body {
/* шрифт */
font-family: "PT San Serif", sans-serif;
/* минимальная ширина */
min-width: 420px;
/* фон и цвет */
background: #060818;
color: #fff;
/* градиент на фоне */
background: linear-gradient(15deg, #d33f34 50%, #a61322 50.1%);
/* высота */
height: 100vh;
}
/* заголовок */
h1 {
/* размер шрифта */
font-size: 5.4rem;
/* ширина */
width: 40rem;
/* отступы */
margin: 2rem auto;
/* выравнивание текста */
text-align: center;
}
/* настройка внешнего вида ссылок */
a,
a:link,
a:active,
a:visited {
/* цвет */
color: #fff;
/* отключаем подчёркивание */
text-decoration: none;
}
/* ссылка при наведении */
a:hover {
/* меняем цвет */
color: #000013;
}
/* блок с добавлением задачи */
.add-task-container {
/* ширина */
width: 50%;
/* выравнивание блоков */
justify-content: space-between;
display: flex;
/* отступы */
margin: 0 auto;
padding: 10px;
/* относительное позиционирование элементов */
position: relative;
/* скрываем части элементов, которые выходят за границу блока */
overflow: hidden;
}
/* основной блок */
.main-container {
/* включаем гибкую вёрстку (размещение элементов внутри блока) */
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
/* колонки — общие настройки */
.columns {
/* гибкая вёрстка */
display: -webkit-box;
display: -ms-flexbox;
display: flex;
/* выравнивание в столбик по левому краю */
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
/* отступы */
margin: 1.6rem auto;
/* ширина */
width: 80%;
/* цвет */
color: #bfc9d4;
}
/* колонка сама по себе */
.column {
/* ширина — во весь блок */
width: 100%;
/* единый размер для всех элементов в колонке */
flex: 1;
/* отступы */
margin: 0 10px;
/* относительное позиционирование */
position: relative;
/* фон */
background: #0e1726;
/* скрываем части элементов, которые не помещаются в блок */
overflow: hidden;
}
/* заголовок колонки */
.column-header {
/* размер шрифта */
font-size: 17px;
/* добавляем линию снизу */
border-bottom: #000013 0.2rem solid;
/* выравнивание текста */
text-align: center;
}
/* список задач */
.task-list {
/* минимальная высота пустого списка */
min-height: 3rem;
}
/* общая настройка списка */
ul {
/* убираем форматирование и буллеты */
list-style-type: none;
/* отступы */
margin: 0;
padding: 0;
}
/* элементы списка */
li {
/* убираем форматирование и буллеты */
list-style-type: none;
}
/* кнопка в колонке */
.column-button {
/* выравнивание текста и отступ */
text-align: center;
padding: 0.1rem;
}
/* кнопка */
.button {
/* делаем жирный шрифт */
font-weight: 700;
/* граница */
border: #000013 0.14rem solid;
/* скругление */
border-radius: 0.2rem;
/* цвет */
color: #000013;
/* отступы */
padding: 0.6rem 1rem;
margin-bottom: 0.3rem;
/* меняем вид курсора при наведении */
cursor: pointer;
}
/* кнопка очистки списка */
.delete-button {
/* фон и отступы */
background-color: #ff4444;
margin: 0.1rem auto 0.6rem auto;
}
/* кнопка очистки при наведении */
.delete-button:hover {
/* фон */
background-color: #fa7070;
}
/* кнопка добавления задачи */
.add-button {
/* фоновый цвет */
background-color: #ffcb1e;
/* отступы */
padding: 0 1rem;
margin-top: 0.6rem;
/* высота и ширина */
height: 3.8rem;
width: 10rem;
}
/* поведение кнопки добавления при наведении */
.add-button:hover {
/* фон */
background-color: #ffdd6e;
}
/* задача */
.task {
/* фон */
background: #1b2e4b;
/* отступы */
margin: 0.4rem;
/* высота */
height: 4rem;
/* гибкая вёрстка и выравнивание содержимого по центру */
display: flex;
-webkit-box-pack: center;
justify-content: center;
/* меняем вид курсора */
cursor: move;
/* скругление границ */
border-radius: 10px;
/* размер шрифта */
font-size: 15px;
}
/* поле ввода задачи */
#taskText {
/* границы */
border: #161515 0.15rem solid;
border-radius: 0.2rem;
/* выравнивание текста */
text-align: center;
/* высота и ширина */
height: 4rem;
width: 90%;
/* отступы */
margin: auto 0.8rem auto 0.1rem;
/* фон */
background: #1b2e4b;
/* цвет */
color: #bfc9d4 !important;
}
/* выравниваем надписи в карточках по центру */
.task p {
margin: auto;
}
Подключаем Dragula JS и свой скрипт
Первое, что нам нужно сделать для перетаскивания, — подключить библиотеку Dragula JS к нашей странице. Можно указать путь к файлу на официальном сайте, можно скачать библиотеку себе и подключить локально — разницы нет. Мы подключим с сайта — для этого в конец HTML-файла добавляем строку:
<script src='https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.js'></script>
Сразу подключим и свой файл со стилями: создадим в той же папке проекта файл script.js
и добавим его после библиотеки.
<script src="script.js"></script>
Наш скрипт пока пустой, наполним его чуть позже, а пока добавим логику для кнопок на странице.
Добавляем обработчики для кнопок на странице
Сделаем так, чтобы добавить задачу можно было двумя способами: по нажатию энтера в поле ввода и по нажатию на кнопку добавления.
Чтобы страница понимала, что мы нажали энтер в поле ввода, нужно ей об этом сказать. Для этого находим в исходном файле такую строку:
<input type="text" maxlength="12" id="taskText" placeholder="Новая задача">
И добавляем к ней обработчик нажатия на энтер:
<input type="text" maxlength="12" id="taskText" placeholder="Новая задача" onkeydown="if (event.keyCode == 13) document.getElementById('add').click()">
Работает это так: если мы в поле ввода нажимаем любую клавишу, страница смотрит на её внутренний код. Если код равен 13, то нажат энтер, а значит, условие выполняется, и вызывается клик по кнопке добавления.
👉 Мы не делаем для этого события отдельный обработчик, а вызываем обработчик нажатия на кнопку.
Теперь добавим обработчик нажатия на кнопку добавления — для этого берём строку <button id="add" class="button add-button">Добавить</button>
и добавляем к ней событие onclick="addTask()"
:
<button id="add" class="button add-button" onclick="addTask()">Добавить </button>
Наконец, добавляем свой обработчик для кнопки очистки колонки с несделанными задачами — точно так же, как и для добавления:
<button class="button delete-button" onclick="emptyTrash()">Очистить</button>
На этом с HTML-файлом всё — сохраняем и идём в скрипт. Визуально на странице ничего не изменилось, потому что всё, что мы сделали, — добавили обработчики, но не прописали логику их работы.
Добавляем скрипт для перетаскивания карточек
Открываем созданный (и пока пустой) файл script.js
и сразу используем возможности Dragula JS. Всё, что нам нужно сделать, — это указать объекты, между которыми разрешено перетаскивание, и пометить, что карточку не нужно после перетаскивания возвращать на исходное место. Остальное библиотека сама сделает за нас: уберёт карточку из одной колонки и поместит в другую. Ещё у нас появится возможность двигать карточки выше или ниже внутри одной колонки — это свойство тоже появляется автоматически после получения доступа к колонкам.
Чтобы всё это получилось, в файле со скриптом пишем:
/* используем Dragula JS */
dragula([
// получаем доступ к колонкам с задачами
document.getElementById("to-do"),
document.getElementById("doing"),
document.getElementById("done"),
document.getElementById("trash")
]);
// разрешаем перетаскивание карточке между колонками
removeOnSpill: false;
Но после сохранения файлов и обновления страницы мы видим, что при перетаскивании у нас появляется копия карточки, которую мы тащим. Она висит справа после колонок и исчезает после перетаскивания, но выглядит это не очень хорошо:
Так происходит потому, что мы не определили стили для перетаскивания, которые использует Dragula JS. Там есть три важных встроенных стиля:
- .gu-mirror — он добавляется к элементу, как только мы начинаем его перетаскивать. Во время перетаскивания на странице создаётся копия этого элемента и привязывается к положения курсора.
- .gu-hide — самый непонятный из всех. Судя по названию, он должен скрывать перемещаемую карточку, но на самом деле он добавляет возможность «раздвигания» границ между карточками или в колонке, чтобы мы могли переместить туда нужную карточку.
- .gu-transit — создаёт виртуальную карточку в той колонке, куда хотим её переместить, чтобы визуально было видно, где карточка появится.
Теперь добавим код в конец файла style.css, чтобы применить эти стили. Сами стили возьмём с официальной страницы библиотеки на Гитхабе:
/* стиль для создания копии карточки при перетаскивании */
.gu-mirror {
position: fixed !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
filter: alpha(opacity=80);
}
/* странный стиль, без которого перетаскивание не работает */
.gu-hide {
display: none !important;
}
/* стиль для создания виртуальной карточки в новой колонке */
.gu-transit {
opacity: 0.2;
filter: alpha(opacity=20);
border: 1px solid #00ff7f !important;
box-shadow: 1px 0 4px 1px #00ff7f !important;
}
Теперь перетаскивание работает как нужно:
Добавляем обработчики событий для кнопок
У нас сейчас в HTML-коде прописан вызов двух обработчиков — добавления задачи и очистки последней колонки. Добавим в скрипт логику их работы, начнём с добавления. Логика такая:
- Берём значение из поля ввода.
- Смотрим, написано там что-то или нет.
- Если написано — добавляем это в первую колонку в виде элемента списка.
- Очищаем поле ввода, чтобы можно было писать новую задачу.
// добавление задачи
function addTask() {
// получаем текст из поля ввода
var inputTask = document.getElementById("taskText").value;
// если текст не пустой — добавляем задачу
if (inputTask != '') {
document.getElementById("to-do").innerHTML += "<li class='task'><p>" + inputTask + "</p></li>";
// очищаем текст в поле ввода
document.getElementById("taskText").value = "";
}
}
С кнопкой очистки последней колонки всё ещё проще — мы просто удаляем HTML-содержимое колонки и тем самым очищаем весь список:
// очищаем колонку с уже сделанными задачами
function emptyTrash() {
// находим колонку по Id и очищаем её HTML-содержимое
document.getElementById("trash").innerHTML = "";
}
Всё, планировщик готов, можно перетаскивать, добавлять и очищать задачи.
Что дальше
Для полноценной локальной работы нам осталось добавить сохранение данных в браузере, чтобы после обновления страницы все задачи не пропадали, а оставались в списках. Этим и займёмся в следующий раз.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Канбан-органайзер для задач</title>
<!-- подключаем стили -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- заголовок -->
<h1>Мои задачи</h1>
<!-- блок создания новой задачи -->
<div class="add-task-container">
<!-- поле ввода текста -->
<input type="text" maxlength="12" id="taskText" placeholder="Новая задача" onkeydown="if (event.keyCode == 13) document.getElementById('add').click()">
<!-- кнопка создания -->
<button id="add" class="button add-button" onclick="addTask()">Добавить </button>
</div>
<!-- блок с задачами -->
<div class="main-container">
<!-- колонки -->
<ul class="columns">
<!-- первая колонка -->
<li class="column to-do-column">
<div class="column-header">
<h4>🌟 Сделать</h4>
</div>
<!-- задачи в колонке -->
<ul class="task-list" id="to-do">
<li class="task">
<p>Убрать на столе</p>
</li>
<li class="task">
<p>Приготовить обед</p>
</li>
<li class="task">
<p>Почитать</p>
</li>
<li class="task">
<p>Выбрать фильм</p>
</li>
</ul>
</li>
<!-- вторая колонка -->
<li class="column doing-column">
<div class="column-header">
<h4>💫 В работе</h4>
</div>
<ul class="task-list" id="doing">
<li class="task">
<p>Сделать презентацию</p>
</li>
<li class="task">
<p>Попить чай</p>
</li>
<li class="task">
<p>Забрать посылку</p>
</li>
</ul>
</li>
<!-- третья колонка -->
<li class="column done-column">
<div class="column-header">
<h4>🏆 Готово</h4>
</div>
<ul class="task-list" id="done">
<li class="task">
<p>Поспать</p>
</li>
<li class="task">
<p>Сделать зарядку</p>
</li>
</ul>
</li>
<!-- четвёртая колонка -->
<li class="column trash-column">
<div class="column-header">
<h4>❌ Не сделано</h4>
</div>
<ul class="task-list" id="trash">
<li class="task">
<p>Разобрать почту</p>
</li>
<li class="task">
<p>Купить телефон</p>
</li>
</ul>
<!-- кнопка очистки того, что не сделано -->
<div class="column-button">
<button class="button delete-button" onclick="emptyTrash()">Очистить</button>
</div>
</li>
</ul>
</div>
<!-- подключаем скрипты -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.js'></script>
<script src="script.js"></script>
</body>
</html>
* {
/* делаем блоки фиксированной ширины во всех элементах */
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
/* общие настройки страницы */
body {
/* шрифт */
font-family: "PT San Serif", sans-serif;
/* минимальная ширина */
min-width: 420px;
/* фон и цвет */
background: #060818;
color: #fff;
/* градиент на фоне */
background: linear-gradient(15deg, #d33f34 50%, #a61322 50.1%);
/* высота */
height: 100vh;
}
/* заголовок */
h1 {
/* размер шрифта */
font-size: 5.4rem;
/* ширина */
width: 40rem;
/* отступы */
margin: 2rem auto;
/* выравнивание текста */
text-align: center;
}
/* блок с добавлением задачи */
.add-task-container {
/* ширина */
width: 50%;
/* выравнивание блоков */
justify-content: space-between;
display: flex;
/* отступы */
margin: 0 auto;
padding: 10px;
/* относительное позиционирование элементов */
position: relative;
/* скрываем части элементов, которые выходят за границу блока */
overflow: hidden;
}
/* поле ввода задачи */
#taskText {
/* границы */
border: #161515 0.15rem solid;
border-radius: 0.2rem;
/* выравнивание текста */
text-align: center;
/* высота и ширина */
height: 4rem;
width: 90%;
/* отступы */
margin: auto 0.8rem auto 0.1rem;
/* фон */
background: #1b2e4b;
/* */
color: #bfc9d4 !important;
}
/* кнопка */
.button {
/* делаем ширный шрифт */
font-weight: 700;
/* граница */
border: #000013 0.14rem solid;
/* скругление */
border-radius: 0.2rem;
/* цвет */
color: #000013;
/* отступы */
padding: 0.6rem 1rem;
margin-bottom: 0.3rem;
/* меняем вид курсора при наведении */
cursor: pointer;
}
/* кнопка добавления задачи */
.add-button {
/* фоновый цвет */
background-color: #ffcb1e;
/* отступы */
padding: 0 1rem;
margin-top: 0.6rem;
/* высота и ширина */
height: 3.8rem;
width: 10rem;
}
/* поведение кнокпи добавления при наведении */
.add-button:hover {
/* фон */
background-color: #ffdd6e;
}
/* кнопка очистки списка */
.delete-button {
/* фон и отступы */
background-color: #ff4444;
margin: 0.1rem auto 0.6rem auto;
}
/* кнопка очистки при наведении */
.delete-button:hover {
/* фон */
background-color: #fa7070;
}
/* основной блок */
.main-container {
/* включаем гибкую вёрстку (размещение элементов внутри блока) */
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
/* колонки - общие настройки */
.columns {
/* гибкая вёрстка */
display: -webkit-box;
display: -ms-flexbox;
display: flex;
/* выравнивание в столбик по левому краю */
-webkit-box-align: start;
-ms-flex-align: start;
align-items: flex-start;
/* отступы */
margin: 1.6rem auto;
/* ширина */
width: 80%;
/* цвет */
color: #bfc9d4;
}
/* колонка сама по себе */
.column {
/* ширина — во весь блок */
width: 100%;
/* единый размер для всех элементов в колонке */
flex: 1;
/* отступы */
margin: 0 10px;
/* относительное позиционирование */
position: relative;
/* фон */
background: #0e1726;
/* скрываем части элементов, которые не помещаются в блок */
overflow: hidden;
}
/* заголовок колонки */
.column-header {
/* размер шрифта */
font-size: 17px;
/* добавляем линию снизу */
border-bottom: #000013 0.2rem solid;
/* выравнивание текста */
text-align: center;
}
/* кнопка в колонке */
.column-button {
/* выравнивание текста и отступ */
text-align: center;
padding: 0.1rem;
}
/* список задач */
.task-list {
/* минимальная высота пустого списка */
min-height: 3rem;
}
/* общая настройка списка */
ul {
/* убираем форматирование и буллеты */
list-style-type: none;
/* отступы */
margin: 0;
padding: 0;
}
/* элементы списка */
li {
/* убираем форматирование и буллеты */
list-style-type: none;
}
/* задача */
.task {
/* фон */
background: #1b2e4b;
/* отступы */
margin: 0.4rem;
/* высота */
height: 4rem;
/* гибкая вёрстка и выравнивание содержимого по центру */
display: flex;
-webkit-box-pack: center;
justify-content: center;
/* меняем вид курсора */
cursor: move;
/* скругление границ */
border-radius: 10px;
/* размер шрифта */
font-size: 15px;
}
/* выравниваем надписи в карточках по центру */
.task p {
margin: auto;
}
/* стиль для создания копии карточки при перетаскивании */
.gu-mirror {
position: fixed !important;
margin: 0 !important;
z-index: 9999 !important;
opacity: 0.8;
filter: alpha(opacity=80);
}
/* странный стиль, без которого перетаскивание не работает */
.gu-hide {
display: none !important;
}
/* стиль для создания виртуальной карточки в новой колонке */
.gu-transit {
opacity: 0.2;
filter: alpha(opacity=20);
border: 1px solid #00ff7f !important;
box-shadow: 1px 0 4px 1px #00ff7f !important;
}
/* используем Dragula JS */
dragula([
// получаем доступ к колонкам с задачами
document.getElementById("to-do"),
document.getElementById("doing"),
document.getElementById("done"),
document.getElementById("trash")
]);
// разрешаем перетаскивание карточке между колонками
removeOnSpill: false;
// добавление задачи
function addTask() {
// получаем текст из поля ввода
var inputTask = document.getElementById("taskText").value;
// если текст не пустой — добавляем задачу
if (inputTask != '') {
document.getElementById("to-do").innerHTML += "<li class='task'><p>" + inputTask + "</p></li>";
// очищаем текст в поле ввода
document.getElementById("taskText").value = "";
}
}
// очищаем колонку с уже сделанными задачами
function emptyTrash() {
// находим колонку по Id и очищаем её HTML-содежримое
document.getElementById("trash").innerHTML = "";
}