Трелло — удобная штука для ведения проектов. Там все задачи представлены в виде колонок, а каждая колонка отвечает за что-то своё. Давайте сделаем нечто подобное, тем более что всё для этого у нас уже есть:
- Bootstrap, с которым мы познакомились, пока создавали страницы с котиками;
- список задач, где можно добавлять новые и удалять ненужные пункты.
Если это объединить, получится что-то похожее на Trello. Логика такая:
- у нас будет несколько колонок на странице (за это отвечает Bootstrap);
- каждая колонка будет отвечать за свои задачи и называться по-своему;
- в каждой колонке можно добавлять и удалять задачи независимо от остальных (а за это отвечает скрипт из прошлого списка задач).
Мы сделаем 4 колонки, но вы для себя можете сделать их столько, сколько нужно. Единственное ограничение — Bootstrap не позволяет делать больше 12 колонок. Надеемся, вам этого хватит.
Готовим колонки и настраиваем стили
За основу возьмём страницу из статьи про котиков и сделаем с ней следующее:
- перенесём в неё стили из страницы со списком задач;
- добавим недостающие стили для заголовков колонок и всей страницы;
- подготовим пустые 4 колонки и настроим их размер под разную ширину экрана.
Если мы всё сделаем правильно, то получится следующее:
<!DOCTYPE html>
<html>
<!-- служебная часть -->
<head>
<!-- заголовок страницы -->
<title>Управление проектами</title>
<!-- настраиваем служебную информацию для браузеров -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- задаём CSS-стили прямо здесь же, чтобы всё было в одном файле -->
<style type="text/css">
/*Задаём общие параметры для всей страницы: шрифт и отступы*/
body {
text-align: center;
margin: 10;
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
}
/* Внешний вид заголовка первого уровня*/
h1 {
margin-bottom: 50px;
font-weight: bold;
}
/* Внешний вид заголовка второго уровня*/
h2 {
font-size: 22px;
}
/* Настраиваем внешний вид поля ввода*/
input {
display: inline-block;
margin: 20px auto;
border: 2px solid #eee;
padding: 10px 20px;
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
}
/*Как будет выглядеть каждый элемент нашего списка*/
.tdItem {
text-align: left;
padding: 10px;
cursor: default;
border-radius: 7px;
}
/*Что произойдёт, когда мы наведём курсор на любую задачу из списка*/
.tdItem:hover {
background-color: lightblue;
}
/*Закончили со стилями*/
</style>
<!-- закрываем служебную часть страницы -->
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<h1>Управление проектами, делами и собой</h1>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<!-- содержимое первой колонки -->
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<!-- содержимое второй колонки -->
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<!-- содержимое третьей колонки -->
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<!-- содержимое четвёртой колонки -->
</div>
</div>
</div>
</body>
<!-- конец всей страницы -->
</html>
Наполняем колонки
У каждой колонки должно быть своё название, поэтому назовём их «Сделать», «Позвонить», «Написать» и «Идеи».
Мы помним, что в каждой колонке должен быть отдельный список задач, поэтому возьмём кусок кода из прошлого материала. Главное, что нам нужно учесть, — что у каждого списка должно быть своё уникальное имя, чтобы мы их не перепутали в процессе. Для этого мы просто добавим цифры от одного до четырёх к названию каждого списка. Например, было «tldDiv», а стало «tld1Div, «tld2Div» и так далее.
В итоге колонки будут выглядеть так:
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Позвонить</h2>
<!-- Поле ввода, куда пишем новые задачи «Позвонить» -->
<div id="tdl2App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Позвонить» -->
<div class="tdl2Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Написать</h2>
<!-- Поле ввода, куда пишем новые задачи «Написать»-->
<div id="tdl3App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Написать» -->
<div class="tdl3Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Идеи</h2>
<!-- Поле ввода, куда пишем новые задачи «Идеи» -->
<div id="tdl4App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Идеи» -->
<div class="tdl4Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
Переносим и расширяем скрипт
Теперь наша задача — взять прошлый скрипт и адаптировать его под наши цели. Нам нужно, чтобы каждая колонка с задачами работала автономно и не зависела от других, поэтому сейчас скрипт увеличится в 4 раза — для каждого списка будет свой код.
Мы всё это понимаем.
Но копипаста быстрее.
Если собрать все изменения, которые нам понадобятся, то получится такой список:
- В самом начале, когда мы заводим переменные под наши задачи, у нас теперь не просто List и Mask, а List1, Mask1, List2, Mask2 и так далее. Это нужно для того, чтобы не смешивать списки задач.
- Поменялось и содержимое этих переменных — теперь там есть цифры, которые показывают, к какому списку они относятся. Важный момент — у нас длина префикса «tdl_» увеличилась на один символ и стала, например, «tdl1_». Это нам нужно будет учесть в середине скрипта.
- Так как списки должны быть автономны, то и индексы, которые мы добавляем к каждому элементу, тоже должны не зависеть друг от друга. Отсюда и переменные вида element_Id_1 и number_Id_1.
Единственное, что осталось прежним, — обработка удаления задачи. В обработчике всё уже изначально организовано так, что скрипт сразу получает все данные, которые ему нужны, независимо от колонки и порядкового номера задачи.
Сам скрипт получается таким:
// Подключаем JQuery
// Пишем скрипт, который будет обрабатывать наши задачи и хранить их на нашем устройстве
// Заводим переменные под наши задачи
var List1 = $('#tdl1App ul');
var Mask1 = 'tdl1_';
var List2 = $('#tdl2App ul');
var Mask2 = 'tdl2_';
var List3 = $('#tdl3App ul');
var Mask3 = 'tdl3_';
var List4 = $('#tdl4App ul');
var Mask4 = 'tdl4_';
// Функция, которая берёт из памяти наши задачи и делает из них список
function showTasks() {
// Узнаём размер хранилища
var Storage_size = localStorage.length;
// Если в хранилище что-то есть…
if (Storage_size > 0) {
// то берём и добавляем это в задачи
for (var i = 0; i < Storage_size; i++) {
var key = localStorage.key(i);
// обрабатываем первый список
if (key.indexOf(Mask1) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List1);
}
// обрабатываем второй список
if (key.indexOf(Mask2) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List2);
}
// обрабатываем третий список
if (key.indexOf(Mask3) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List3);
}
// обрабатываем четвёртый список
if (key.indexOf(Mask4) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List4);
}
}
}
}
// Сразу вызываем эту функцию, вдруг в памяти уже остались задачи с прошлого раза
showTasks();
// Следим, когда пользователь напишет новую задачу в первое поле ввода и нажмёт Enter
$('#tdl1App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_1 = 0;
List1.children().each(function (index, el) {
var element_Id_1 = $(el).attr('data-itemid').slice(5);
if (element_Id_1 > number_Id_1)
number_Id_1 = element_Id_1;
})
number_Id_1++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask1 + number_Id_1, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask1 + number_Id_1)
.text(str).appendTo(List1);
}
});
// Следим, когда пользователь напишет новую задачу во второе поле ввода и нажмёт Enter
$('#tdl2App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_2 = 0;
List2.children().each(function (index, el) {
var element_Id_2 = $(el).attr('data-itemid').slice(5);
if (element_Id_2 > number_Id_2)
number_Id_2 = element_Id_2;
})
number_Id_2++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask2 + number_Id_2, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask2 + number_Id_2)
.text(str).appendTo(List2);
}
});
// Следим, когда пользователь напишет новую задачу в третье поле ввода и нажмёт Enter
$('#tdl3App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_3 = 0;
List3.children().each(function (index, el) {
var element_Id_3 = $(el).attr('data-itemid').slice(5);
if (element_Id_3 > number_Id_3)
number_Id_3 = element_Id_3;
})
number_Id_3++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask3 + number_Id_3, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask3 + number_Id_3)
.text(str).appendTo(List3);
}
});
// Следим, когда пользователь напишет новую задачу в четвёртое поле ввода и нажмёт Enter
$('#tdl4App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_4 = 0;
List4.children().each(function (index, el) {
var element_Id_4 = $(el).attr('data-itemid').slice(5);
if (element_Id_4 > number_Id_4)
number_Id_4 = element_Id_4;
})
number_Id_4++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask4 + number_Id_4, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask4 + number_Id_4)
.text(str).appendTo(List4);
}
});
// По клику на задаче — убираем её из списка
$(document).on('click', '.tdItem', function (e) {
// Находим задачу, по которой кликнули
var jet = $(e.target);
// Убираем её из памяти
localStorage.removeItem(jet.attr('data-itemid'));
// и убираем её из списка
jet.remove();
})
// Закончился основной скрипт
Собираем готовую страницу
Нам осталось собрать всё вместе и проверить, как это работает. Если будете добавлять колонки — не забудьте про новые названия для переменных и про имена элементов в самих колонках. На текущем этапе мы добавляем всё вручную, но в будущей версии сделаем генератор нужного числа колонок.
<!DOCTYPE html>
<html>
<!-- служебная часть -->
<head>
<!-- заголовок страницы -->
<title>Управление проектами</title>
<!-- настраиваем служебную информацию для браузеров -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- задаём CSS-стили прямо здесь же, чтобы всё было в одном файле -->
<style type="text/css">
/*Задаём общие параметры для всей страницы: шрифт и отступы*/
body {
text-align: center;
margin: 10;
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
}
/* Внешний вид заголовка первого уровня*/
h1 {
margin-bottom: 50px;
font-weight: bold;
}
/* Внешний вид заголовка второго уровня*/
h2 {
font-size: 22px;
}
/* Настраиваем внешний вид поля ввода*/
input {
display: inline-block;
margin: 20px auto;
border: 2px solid #eee;
padding: 10px 20px;
font-family: Verdana, Arial, sans-serif;
font-size: 16px;
}
/*Как будет выглядеть каждый элемент нашего списка*/
.tdItem {
text-align: left;
padding: 10px;
cursor: default;
border-radius: 7px;
}
/*Что произойдёт, когда мы наведём курсор на любую задачу из списка*/
.tdItem:hover {
background-color: lightblue;
}
/*Закончили со стилями*/
</style>
<!-- закрываем служебную часть страницы -->
</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<h1>Управление проектами, делами и собой</h1>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Сделать</h2>
<!-- Поле ввода, куда пишем новые задачи «Сделать»-->
<div id="tdl1App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Сделать» -->
<div class="tdl1Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Позвонить</h2>
<!-- Поле ввода, куда пишем новые задачи «Позвонить» -->
<div id="tdl2App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Позвонить» -->
<div class="tdl2Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Написать</h2>
<!-- Поле ввода, куда пишем новые задачи «Написать»-->
<div id="tdl3App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Написать» -->
<div class="tdl3Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-3 col-xl-3">
<h2 class="todo__caption">Идеи</h2>
<!-- Поле ввода, куда пишем новые задачи «Идеи» -->
<div id="tdl4App">
<input type="text" class="form-control" placeholder="Новая задача">
<!-- Создаём пока ещё пустой список «Идеи» -->
<div class="tdl4Div">
<ul class="List list-unstyled">
<!-- Тут появятся наши задачи, когда мы их добавим -->
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<!-- Пишем скрипт, который будет обрабатывать наши задачи и хранить их на нашем устройстве -->
<script>
// Заводим переменные под наши задачи
var List1 = $('#tdl1App ul');
var Mask1 = 'tdl1_';
var List2 = $('#tdl2App ul');
var Mask2 = 'tdl2_';
var List3 = $('#tdl3App ul');
var Mask3 = 'tdl3_';
var List4 = $('#tdl4App ul');
var Mask4 = 'tdl4_';
// Функция, которая берёт из памяти наши задачи и делает из них список
function showTasks() {
// Узнаём размер хранилища
var Storage_size = localStorage.length;
// Если в хранилище что-то есть…
if (Storage_size > 0) {
// то берём и добавляем это в задачи
for (var i = 0; i < Storage_size; i++) {
var key = localStorage.key(i);
// обрабатываем первый список
if (key.indexOf(Mask1) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List1);
}
// обрабатываем второй список
if (key.indexOf(Mask2) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List2);
}
// обрабатываем третий список
if (key.indexOf(Mask3) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List3);
}
// обрабатываем четвёртый список
if (key.indexOf(Mask4) == 0) {
// и делаем это элементами списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', key)
.text(localStorage.getItem(key))
.appendTo(List4);
}
}
}
}
// Сразу вызываем эту функцию, вдруг в памяти уже остались задачи с прошлого раза
showTasks();
// Следим, когда пользователь напишет новую задачу в первое поле ввода и нажмёт Enter
$('#tdl1App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_1 = 0;
List1.children().each(function (index, el) {
var element_Id_1 = $(el).attr('data-itemid').slice(5);
if (element_Id_1 > number_Id_1)
number_Id_1 = element_Id_1;
})
number_Id_1++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask1 + number_Id_1, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask1 + number_Id_1)
.text(str).appendTo(List1);
}
});
// Следим, когда пользователь напишет новую задачу во второе поле ввода и нажмёт Enter
$('#tdl2App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_2 = 0;
List2.children().each(function (index, el) {
var element_Id_2 = $(el).attr('data-itemid').slice(5);
if (element_Id_2 > number_Id_2)
number_Id_2 = element_Id_2;
})
number_Id_2++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask2 + number_Id_2, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask2 + number_Id_2)
.text(str).appendTo(List2);
}
});
// Следим, когда пользователь напишет новую задачу в третье поле ввода и нажмёт Enter
$('#tdl3App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_3 = 0;
List3.children().each(function (index, el) {
var element_Id_3 = $(el).attr('data-itemid').slice(5);
if (element_Id_3 > number_Id_3)
number_Id_3 = element_Id_3;
})
number_Id_3++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask3 + number_Id_3, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask3 + number_Id_3)
.text(str).appendTo(List3);
}
});
// Следим, когда пользователь напишет новую задачу в четвёртое поле ввода и нажмёт Enter
$('#tdl4App input').on('keydown', function (e) {
if (e.keyCode != 13) return;
var str = e.target.value;
e.target.value = "";
// Если в поле ввода было что-то написано — начинаем обрабатывать
if (str.length > 0) {
var number_Id_4 = 0;
List4.children().each(function (index, el) {
var element_Id_4 = $(el).attr('data-itemid').slice(5);
if (element_Id_4 > number_Id_4)
number_Id_4 = element_Id_4;
})
number_Id_4++;
// Отправляем новую задачу сразу в память
localStorage.setItem(Mask4 + number_Id_4, str);
// и добавляем её в конец списка
$('<li></li>').addClass('tdItem')
.attr('data-itemid', Mask4 + number_Id_4)
.text(str).appendTo(List4);
}
});
// По клику на задаче — убираем её из списка
$(document).on('click', '.tdItem', function (e) {
// Находим задачу, по которой кликнули
var jet = $(e.target);
// Убираем её из памяти
localStorage.removeItem(jet.attr('data-itemid'));
// и убираем её из списка
jet.remove();
})
// Закончился основной скрипт
</script>
</body>
<!-- конец всей страницы -->
</html>
Что дальше
Чтобы этот продукт стал ближе к идеалу, можно сделать так:
- с помощью классов и объектов упорядочить создание новых колонок — не копипастить код, а делать всё через объекты;
- добавить возможность перетаскивать задачи между колонками;
- сделать комментарии к каждой задаче;
- разрешить изменение заголовков у колонок прямо на странице (contentEditable нам в помощь);
- настроить фон.
Скоро доберёмся и до этого!