Создание подзадач для основной задачи
Возникла проблема. Я создаю приложение на js. В нем присутствует: добавление задачи, редактирование название, изменение приоритета и изменение цвета при выполнении . И вот какая сложность появилась. Как реализовать подзадачи основной задачи ? То есть жмакнуть на саму задачу(на скрине это work) и перейти в неё, а там уже добавлять подзадачи к ней + при выходе/перезагрузке страницы все должно оставаться без задействования базы данных (я использую память браузера). Переварить и сообразить не как не могу . Нужна помощь спецов.

Код всего js (не самый лучший , пока учусь)
const form = document.querySelector('form');
const ul = document.querySelector('tbody');
const input = document.getElementById('item');
const addItem = document.getElementById('addItem');
let itemsArray = localStorage.getItem('items') ? JSON.parse(localStorage.getItem('items')) : [];
// ------------------- add tr ---------------------
const liMaker = (text) => {
const li = document.createElement('tr');
li.innerHTML = `
<td>
<input type="checkbox" class="checkbox" id="qwe">
</td>
<td class="name-list">
<p class="p_input" id="Name"><a href="#">${text}</a></p>
</td>
<td class="editing-list">
<div class="editing">
<img src="images/UpDown.png" alt="UpDown" onclick="change(this)" class="trUp">
<div class="vl"></div>
<img src="images/pen.png" alt="editing" class="editing-name">
<div class="vl"></div>
<a href="#"><img src="images/basket.png" alt="basket" class="clear"></a>
</div>
</td>`;
function clickListener(evt) {
if (evt.target.className == "clear") {
let tr = evt.currentTarget;
tr.removeEventListener("click", clickListener);
window.onunload = function (evt) {
localStorage.setItem("items", JSON.stringify(itemsArray));
}
itemsArray.splice(itemsArray.indexOf(text), 1);
tr.parentElement.removeChild(tr);
}
}
li.addEventListener("click", clickListener)
ul.appendChild(li);
t3();
}
// ------------------- function ---------------------
form.addEventListener('submit', function (e) {
e.preventDefault();
if (itemsArray.includes(input.value)) {
input.value = "";
return;
}
itemsArray.push(input.value);
liMaker(input.value);
input.value = "";
});
itemsArray.forEach(item => {
liMaker(item);
});
window.onunload = function (evt) {
localStorage.setItem("items", JSON.stringify(itemsArray));
}
// --------- checked -----------
function t3() {
let k = document.querySelectorAll('.checkbox');
let td = document.querySelectorAll('tr');
for (var i = 0; i < k.length; i++) {
k[i].onchange = function () {
Array.from(k).map((v, i) => {
if (v.checked) {
Array.from(td)[i].style.cssText = "background-color:green";
}
else {
Array.from(td)[i].style.cssText = "";
}
})
}
}
}
// ---------- change Name ---------
document.addEventListener(`click`, e => {
if (e.target.classList.contains(`editing-name`)) {
let paragraph = e.target.parentElement.parentElement.previousElementSibling.firstElementChild;
let x = document.createElement('input');
x.classList.add('NameCh');
paragraph.innerHTML = ' ';
paragraph.append(x);
x.addEventListener('keydown', function (e) {
if (e.keyCode === 13) {
paragraph.innerHTML = x.value;
}
});
}
})
// ----------------- trUp -------------------
function change(i) {
var tr1, tr2;
if (!(tr1 = document.getElementsByTagName('tr')[0]) || !(tr2 = i.closest('tr')))
return;
tr2.parentNode.insertBefore(tr2, tr1);
}
body{
font-family: 'Lato', sans-serif;
}
a {
text-decoration: none;
color:black;
}
.container {
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
left: 50%;
}
.notebook{
width: 1000px;
min-height: 400px;
border: 1px solid black;
border-radius: 0 0 20px 20px;
}
/* -------------------- header -------------------- */
.header{
background: #4677b2;
height: 70px;
}
.icon{
float: left;
width: 100px;
height: 70px;
}
.icon img{
width: 50px;
height: 50px;
margin: 10px 25px;
}
.icon a{
text-align: center;
}
.name{
display: inline-block;
width: 760px;
color: #fff;
}
.editing{
display:block;
}
.editing img{
width: 35px;
height: 35px;
}
.editing p {
display: inline-block;
margin: 5px;
}
.editing-header{
display:inline-block;
margin-top: 15px;
}
.editing-header img{
width: 35px;
height: 35px;
}
.editing-header p {
display: inline-block;
margin: 5px;
}
/* -------------------- input -------------------- */
.input{
height: 70px;
background: goldenrod;
background: #cfcfcf;
}
.input-note{
display: inline-block;
width: 870px;
}
.input-note input{
margin-top: 12px;
}
.input-name{
height: 40px;
width: 700px;
border: 1px solid #707070;
}
#button-input-value{
float: right;
width: 163.3px;
margin-top: 12px;
height: 44px;
background: #76ad8e;
border-radius: 0 10px 10px 0;
border: 1px solid #707070;
}
/* -------------------- list -------------------- */
.list{
height:auto;
}
.list-table{
position: relative;
width: 1000px;
height: auto;
}
table{
border-radius: 10px;
margin: 5px;
}
.list-table table{
margin-left: 7px;
border-collapse: collapse;
border: 2px solid #fff;
border-radius:0 0 20px 20px;
}
td ,tr {
padding: 0;
border: 1px solid black;
}
td{
height: 50px;
}
.checkbox{
width: 80px;
}
.name-list{
width: 710px;
}
.editing-list{
width: 185px;
}
#qwe {
transform:scale(1.3);
cursor:pointer;
}
p{
font-size: 18px;
margin-left: 15px;
}
.vl {
display: inline-block;
margin: 0 5px ;
border-left: 1px solid black;
height: 30px;
}
.editing{
display:none;
}
tr:hover .editing{
display:block;
position:absolute;
margin-left:10px ;
margin-top: -15px;
z-index:9999;
}
.editing-name{
cursor: pointer;
}
.nameInput{
height: 20px;
font-size: 16px;
}
form{
display: inline-block;
}
.trUp{
cursor: pointer;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notebook</title>
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<div class="notebook">
<!-- ----------- header ----------- -->
<div class="header">
<div class="icon">
<a href="index.html"><img src="images/icon.png" alt="icon"></a>
</div>
<div class="name">
<h2>For home</h2>
</div>
</div>
<!-- ----------- input ----------- -->
<div class="input">
<div class="icon">
<img src=" images/plus.png" alt="icon" class="plus">
</div>
<form class="input-note">
<input type="text" class="input-name" id="item">
<button id="button-input-value" name="submit">Add Task</button>
</form>
</div>
<!-- ----------- list ----------- -->
<div class="list">
<div class="list-table">
<table>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>
Ответы (1 шт):
itemsArray - массив, где вы храните данные вида ["task-1", "task-2", "task-3"]
Должен (как вариант) быть таким:
const itemsArray = [
{
text: "task-1",
checked: true, // Галочка была отмечена? При создании tr покрасить зеленым
sub: [ // Список подзадач
{ text: "task-1-1", checked: true },
{ text: "task-1-2", checked: false },
{ text: "task-1-3", checked: false },
]
},
{
text: "task-2",
checked: false,
sub: [
{ text: "task-2-1", checked: true },
]
},
];
И тогда, заменить в коде:
if (itemsArray.includes(text)) { ...
↓
let CURR_TASK_SPACE = itemsArray; // глобальный
/* Менять на соответствующий вложенный sub или обратно на itemsArray
при входе-выходе из подзадач.
Так, чтобы всегда ссылался на текущий массив отображаемых задач. */
// где-то внутри, отсеивая повторы...
if (task_exists(text)) { ...
function task_exists(text) {
return CURR_TASK_SPACE.some(task => task.text === text);
}
Т.к. уровень вложенности всего один, не нужно запоминать родительский объект задач. И можно тупо обновить всю таблицу, заново циклом добавив все задачи из CURR_TASK_SPACE в виде новых tr. Да, не "оптимально", но и задач-то будет всего десяток-два.
В таком случае, нужна функция а-ля create_table(data) которая берет массив задач и всё пихает в таблицу в виде tr (которая будет вызываться один раз при получении данных из localStorage, и в дальнейшем, при переключениях к подзадачам и обратно).
А при клике на задачу, надо уметь находить её подзадачи. Т.к. названия задач уникальные, можно прочитать значение прямо из HTML:
function find_sub(tr) {
if (режим_редакции) return;
let paragraph = tr.querySelector(".параграф-с-текстом-задачи");
return CURR_TASK_SPACE.find(task => task.text === paragraph.textContent);
// Если уже находится в sub, или такой задачи по какой-то причине нет,
// не будет ошибки, просто вернет undefined
// Если sub найден, функция, которая вызвала find_sub(),
// • выставит CURR_TASK_SPACE = sub;
// • позовет create_table(sub);
}
localStorage.setItem("...", JSON.stringify()) — вместо прямых обращений в коде, это можно завернуть в такие функции:
function get_data() {
let data = localStorage.getItem("DATA");
return data ? JSON.parse(data) : [];
}
function set_data(data) {
localStorage.setItem(JSON.stringify(data));
}
Может захочется для тестов временно подсунуть другие данные, или заменить localStorage на что-то другое. Это удобнее делать, если всё происходит в одном месте.
P.s. Вообще-то itemsArray не очень хорошее название: Может случиться так, что массив в будущем превратится в другую структуру. А в коде уже везде разбросано слово "Array" )
P.s.s. Удаление, редакцию, поднятие вверх, краску при checked - это всё по хорошему нужно реализовать через делегирование событий, чтобы при вставке новых строк таблицы не думать про добавление и обработку событий.
tbody.addEventListener("click", function (e) {
let target = e.target;
let tr = target.closest("tr");
if (!tr) return;
if (target.matches(".класс-редакции")) return task_edit(tr);
if (target.matches(".класс-всплытия")) return task_move_top(tr);
if (target.matches(".класс-удаления")) return task_delete(tr);
});
function task_edit() { ... }
function task_delete() { ... }
UPD: Начал переписывать ваш код, пытаясь не трогать HTML и стили (добавил thead в таблицу и 3 новых стиля), в итоге от начального кода ничего не осталось) Но что-то рабочее - получилось организовать:
let tbody = document.querySelector('tbody');
/*****/
let DATA = get_data(); // Получает основные данные из localStorage
let CURR_TASK_SPACE = DATA;
/* Изменяемая ссылка на массив: группу задач, с которой "сейчас" работаем.
Функции таблицы (создание / удаление / редакция) оперируют его элементами
Сейчас это основные задачи, позже может указывать на вложенный массив подзадач
*/
update_table(); // Рисует таблицу на основе CURR_TASK_SPACE
window.onunload = function () {
set_data(DATA);
};
/***/
function get_data() {
/* Объект для тестов, в реальном коде можно превратить в комментарий,
чтобы иметь представление, с какими данными вообще работаем */
return [
{
text: "task-1",
checked: false,
sub: [
{ text: "task-1-1", checked: true },
{ text: "task-1-2", checked: false },
{ text: "task-1-3", checked: false },
]
},
{
text: "task-2",
checked: true,
sub: [
{ text: "task-2-1", checked: false },
]
},
];
let data = localStorage.getItem("DATA");
return data ? JSON.parse(data) : [];
}
function set_data(data) {
return; /* Заглушка */
localStorage.setItem(JSON.stringify(data));
}
/***/
function update_table() {
/* CURR_TASK_SPACE = [
{
text: "str",
checked: Boolean,
},
...
] */
tbody.textContent = "";
CURR_TASK_SPACE.forEach(item => {
create_task(item.text, item.checked);
});
}
function create_task(text, checked) {
let tr = document.createElement('tr');
/* ниже, id="qwe", id="Name" надо убрать или заменить на классы.
id по определению должен быть уникальным на странице.
Оставил, чтобы не сломать ваш CSS */
tr.innerHTML = `
<td>
<input type="checkbox" class="checkbox" id="qwe" ${checked ? "checked" : ""}>
</td>
<td class="name-list">
<p class="p_input" id="Name"><a href="#">${text}</a></p>
</td>
<td class="editing-list">
<div class="editing">
<img src="images/UpDown.png" alt="UpDown" class="trUp">
<div class="vl"></div>
<img src="images/pen.png" alt="editing" class="editing-name">
<div class="vl"></div>
<a href="#"><img src="images/basket.png" alt="basket" class="clear"></a>
</div>
</td>
`;
if (checked) tr.classList.add("done");
// Вместо style.backgroundColor = "green"; добавил класс,
// т.к. цвет меняется не только тут.
// Чтобы при необходимости, редактировать только стили в одном месте.
tbody.appendChild(tr);
}
/*****/
// С обработкой и отображением данных разобрались, теперь функционал
onsubmit_add_task();
task_user_features();
sub_task_features();
/***/
function get_task_data(tr, remove) {
/* В массиве CURR_TASK_SPACE хранятся объекты задач
всегда в том же порядке, что и текущие tr
Берет tr, выдает соответствующий ему объект
Если передали второй аргумент (опционально),
заодно и удалит объект из хранилища */
let index = [].indexOf.call(tr.parentNode.children, tr);
// Номер полученного tr среди всех соседних.
return remove ? CURR_TASK_SPACE.splice(index, 1)[0] : CURR_TASK_SPACE[index];
// Справка: splice удаляет элемент из массива и возвращает
// массив удаленных элементов, откуда достается [0]-й (как-раз удаленный) элемент
// Т.е. функция всегда возвращает объект задачи по tr, что от неё и нужно
}
/***/
function onsubmit_add_task() {
let form = document.querySelector('form');
let input = document.getElementById('item');
// Перенес объявление ближе к коду, где они используются.
// Не обязательно вывалить все переменные одним блоком, прямо в самом начале.
form.addEventListener('submit', function (e) {
e.preventDefault();
let text = input.value;
input.value = "";
if (task_exists(text)) return;
create_task(text);
CURR_TASK_SPACE.push({
text: text,
checked: false
});
// Создается и добавляется новый объект текущей задачи
// Без списка подзадач sub: [], он будет добавлен по мере необходимости
});
/***/
function task_exists(text) {
// возвращает true, если найдет хотя бы один task с совпадающим текстом
return CURR_TASK_SPACE.some(task => task.text === text);
}
}
function task_user_features() {
tbody.addEventListener("change", function (e) {
if (e.target.matches(".checkbox")) {
let tr = e.target.closest("tr");
toggle_checked(tr, e.target.checked);
}
});
tbody.addEventListener("click", function (e) {
let target = e.target;
let tr = target.closest("tr");
if (!tr) return;
if (target.matches(".editing-name")) return task_edit_mode(tr);
if (target.matches(".trUp")) return task_move_top(tr);
if (target.matches(".clear")) return task_delete(tr);
});
/***/
/* помимо изменений в HTML, также вносят изменения в CURR_TASK_SPACE */
function task_edit_mode(tr) {
let a = tr.querySelector(".p_input a");
a.setAttribute("contentEditable", true);
// Стили можно добавить на .p_input a[contentEditable] { border: 1px solid }
a.addEventListener("keydown", function _tmp(e) {
if (e.keyCode === 13) { // "Enter"
a.removeAttribute("contentEditable");
a.removeEventListener("keydown", _tmp);
get_task_data(tr).text = a.textContent;
}
});
}
function task_move_top(tr) {
tbody.insertAdjacentElement("afterBegin", tr);
let data = get_task_data(tr, "remove"); // удаляет объект из середины
CURR_TASK_SPACE.unshift(data); // пихает в начало
}
function task_delete(tr) {
tr.parentNode.removeChild(tr);
// вместо вызова get_task_data(tr, true);
get_task_data(tr, "remove");
// Строка "remove" в логическом контексте тоже true.
// Зато понятно, чего хотим от функции, вместо абстрактного true
}
function toggle_checked(tr, checked) {
/* Добавляет или удаляет класс "done" на tr, в зависимости от значения checked */
tr.classList.toggle("done", checked);
// checked == true ? добавится, иначе - удалится.
get_task_data(tr).checked = checked;
}
}
function sub_task_features() {
let thead = document.querySelector("thead");
entering_sub_task_mode();
back_to_primary_tasks();
/***/
function entering_sub_task_mode() {
let primary_task = document.querySelector("#selected-task-text");
// При входе в режим подзадач, копировать сюда текст основной задачи
tbody.addEventListener("click", function (e) {
let task_name = e.target.closest("td.name-list");
if (!task_name) return; // кликнули не на td с названием задачи
if (thead.matches(".sub-task-mode")) return;
// и так находимся в подзадачах, нет вложенных подзадач
let edit_mode = task_name.querySelector("a[contentEditable]");
if (edit_mode) return; // задачу редактируют
/*** Входим в подзадачи */
thead.classList.add("sub-task-mode");
primary_task.textContent = task_name.textContent;
let tr = task_name.parentNode;
let data = get_task_data(tr);
if (!data.sub) data.sub = []; // подзадачи еще не создавали, создает.
CURR_TASK_SPACE = data.sub;
// начинает ссылаться на массив подзадач
update_table();
});
}
function back_to_primary_tasks() {
let back = document.querySelector("#back");
back.addEventListener("click", function () {
thead.classList.remove("sub-task-mode");
CURR_TASK_SPACE = DATA;
// снова начинает ссылаться на большой массив задач
update_table();
});
}
}
thead {
display: none;
}
thead.sub-task-mode {
display: table-row-group;
}
tr.done {
background-color: green;
}
/* дальше - не трогал */
body {
font-family: 'Lato', sans-serif;
}
a {
text-decoration: none;
color: black;
}
.container {
transform: translate(-50%, -50%);
position: absolute;
top: 50%;
left: 50%;
}
.notebook {
width: 1000px;
min-height: 400px;
border: 1px solid black;
border-radius: 0 0 20px 20px;
}
/* -------------------- header -------------------- */
.header {
background: #4677b2;
height: 70px;
}
.icon {
float: left;
width: 100px;
height: 70px;
}
.icon img {
width: 50px;
height: 50px;
margin: 10px 25px;
}
.icon a {
text-align: center;
}
.name {
display: inline-block;
width: 760px;
color: #fff;
}
.editing {
display: block;
}
.editing img {
width: 35px;
height: 35px;
}
.editing p {
display: inline-block;
margin: 5px;
}
.editing-header {
display: inline-block;
margin-top: 15px;
}
.editing-header img {
width: 35px;
height: 35px;
}
.editing-header p {
display: inline-block;
margin: 5px;
}
/* -------------------- input -------------------- */
.input {
height: 70px;
background: goldenrod;
background: #cfcfcf;
}
.input-note {
display: inline-block;
width: 870px;
}
.input-note input {
margin-top: 12px;
}
.input-name {
height: 40px;
width: 700px;
border: 1px solid #707070;
}
#button-input-value {
float: right;
width: 163.3px;
margin-top: 12px;
height: 44px;
background: #76ad8e;
border-radius: 0 10px 10px 0;
border: 1px solid #707070;
}
/* -------------------- list -------------------- */
.list {
height: auto;
}
.list-table {
position: relative;
width: 1000px;
height: auto;
}
table {
border-radius: 10px;
margin: 5px;
}
.list-table table {
margin-left: 7px;
border-collapse: collapse;
border: 2px solid #fff;
border-radius: 0 0 20px 20px;
}
td,
tr {
padding: 0;
border: 1px solid black;
}
td {
height: 50px;
}
.checkbox {
width: 80px;
}
.name-list {
width: 710px;
}
.editing-list {
width: 185px;
}
#qwe {
transform: scale(1.3);
cursor: pointer;
}
p {
font-size: 18px;
margin-left: 15px;
}
.vl {
display: inline-block;
margin: 0 5px;
border-left: 1px solid black;
height: 30px;
}
.editing {
display: none;
}
tr:hover .editing {
display: block;
position: absolute;
margin-left: 10px;
margin-top: -15px;
z-index: 9999;
}
.editing-name {
cursor: pointer;
}
.nameInput {
height: 20px;
font-size: 16px;
}
form {
display: inline-block;
}
.trUp {
cursor: pointer;
}
<link href="https://fonts.googleapis.com/css2?family=Lato&display=swap" rel="stylesheet">
<div class="container">
<div class="notebook">
<!-- ----------- header ----------- -->
<div class="header">
<div class="icon">
<a href="index.html"><img src="images/icon.png" alt="icon"></a>
</div>
<div class="name">
<h2>For home</h2>
</div>
</div>
<!-- ----------- input ----------- -->
<div class="input">
<div class="icon">
<img src=" images/plus.png" alt="icon" class="plus">
</div>
<form class="input-note">
<input type="text" class="input-name" id="item">
<button id="button-input-value" name="submit">Add Task</button>
</form>
</div>
<!-- ----------- list ----------- -->
<div class="list">
<div class="list-table">
<table>
<thead>
<tr>
<th id="back">←</th>
<th colspan="2" id="selected-task-text"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>