A localStorage-ot mint helyi adatbázist használjuk most a feladataink tartós (persist) eltárolására. Ide kívánkozik ugyanakkor, hogy a böngészők privát módjánál (így a Firefoxnál alapból) a localStorage is úgy működik, mint a sessionStorage, azaz nem tárol csak az adott munkamenet alatt. Ettől függetlenül a közösség érdekében mindenki használjon Firefox-ot.
Behúzzuk a font-awesome-ot a kuka ikon miatt. Az oldal 4 nagyobb különálló div-re tagozódik. A todo__header-ben a dátumok mellett lesz egy form, hogy egybe fogja a beviteli inputot és a beviteli button-t. Azért jó form-ot használni mert ekkor input ENTER-rel és a plusz gombbal is el tud majd menni a bevitt feladat egyetlen eseményfigyelőn keresztül. A pending, azaz elvégzendő feladatok azért vannak kikommentelve, mert ezeket a feladatokat majd mindig a JS hajtja végre, minden esetben újra és újra elő lesznek állítva (renderelve) a feladat divek.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To do list</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Daily To-Do list manager</h1>
<div class="todo-container">
<div class="todo__header">
<h2 class="day"></h2>
<h2 class="date"></h2>
<form class="input-box">
<input class="input" type="text" placeholder="Take the garbage out">
<button type="submit" class="add">+</button>
</form>
</div>
<p>You have <span class="pending-count"></span> pending items</p>
<div class="chill"></div>
<div class="pending-todos-box">
<!-- <div class="todo" data-id="123456789">
<div class="input-label-box">
<input type="checkbox">
<label>Task for today</label>
</div>
<span class="trash"></span>
</div> -->
</div>
<div class="completed-todos-box hide">
<!-- <p>Completed tasks: 50%</p> -->
</div>
<div class="todo__footer">
<button class="hide-completed">Show Complete</button>
<button class="clear">Clear All</button>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
Ezt az alap beállítást majd minden esetben használom, a CSS guruk azt mondják, hogy pseudo elemek esetén ez a korrekt box-sizing beállítás. A body-t is jó általában kifeszíteni, mert önmagában nincs magassága, csak a gyerek elemek miatt.
* {
margin: 0;
padding: 0;
}
*, *::before, *::after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
}
body {
min-height: 100vh;
font-family: sans-serif;
background: linear-gradient(violet, green);
color: rgba(0, 0, 0,.4);
}
Ezek layout beállítások.
h1 {
padding: 2rem;
text-align: center;
color: #fff;
}
h2 {
color: rgba(255,0,0, .5);
font-weight: 500;
}
p {
margin: 0.5rem 0;
}
.day {
margin-top: 1rem;
}
A fő containert a 'margin: 0 auto' állítja középre és a flex miatt tudjuk majd a benne lévő elemeket könnyedén mozgatni.
.todo-container {
width: 30rem;
height: 82vh;
border-radius: 3px;
background-color: #ececec;
display: flex;
justify-content: space-between;
flex-direction: column;
margin: 0 auto;
padding: 1.5rem;
}
A form (.input-box) flex tulajdonsága miatt tudjuk a gyerek beviteli input mezőt a 'flex:1'-el kifeszíteni és megformázzuk benne a placeholder-t.
.input-box {
display: flex;
margin-top: 1rem;
}
.input {
flex: 1;
margin-right: 0.5rem;
padding-left: 0.5rem;
border: 1px solid rgba(0, 0, 0,.3);
font-style: italic;
color: rgba(0, 0, 0,.3);
border-radius: 3px;
font-size: 1rem;
}
Az elvégzendő feladatok div felveszi a rendelkezésére álló összes helyet (flex:1) és az egyes feladatok div-jében pedig azért használunk position:relativ-et, hogy a pseudo elemet majd tudjuk mozgatni hozzá képest. A show-in class majd a legelső feladat elemre vonatkozik, mert az majd ezzel az animációval fog megérkezni.
.pending-todos-box {
flex: 1;
}
.show-in {
animation-name: show-in;
animation-duration: 1s;
}
.todo {
background-color: rgba(0, 0, 0,.1);
padding: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
position: relative;
}
.input-label-box {
display: flex;
align-items: center;
}
@keyframes show-in {
0%{
opacity: 0;
width: 0
}
100% {
opacity: 1;
width: 100%;
}
}
A kukát most egy pseudo elemmel hozzuk létre, ez egy font-awesome-elem, ezért a content unicode mellett most a FontAwesome font-family-t is használni kell. Az input végi beigazításhoz kell a position:absolut és a right:0 és a line-height-al állítjuk a kukát függőleges irányban középre, mégpedig úgy hogy a szülő elem magasságát adjuk neki, ilyenkor a karakter (jelen esetben ikon) középre kerül.
.todo:hover.todo::after {
position: absolute;
content: "\f014";
font-family: FontAwesome;
right: 0;
background-color: red;
height: 100%;
width: 2.5rem;
cursor: pointer;
text-align: center;
line-height: 2.5rem;
color: #fff;
animation-name: fade-in;
animation-duration: 1s;
}
@keyframes fade-in {
0% {
width: 0;
content:'';
}
100% {
width: 2.5rem;
content: "\f014";
}
}
Mivel a pseudo elemet nem tudjuk kattintani, ezért a kuka felé rakunk egy span elemet, amire majd felírjuk az eseménykezelőt.
.trash {
width: 2.5rem;
height: 1.5rem;
z-index: 10;
cursor: pointer;
}
A következő elemek a feladat pipa négyzetek és a bepipálás design-olására szolgálnak. A bepipálás is egy pseudo elemmel történik, mégpedig úgy, hogy amikor megtörténik a bepipálás, akkor az eredeti design eltűnik és rajzolunk egy újat a \2713-as unicode-al.
input[type='checkbox'] {
width: 1.5rem;
height: 1.5rem;
cursor: pointer;
}
input[type='checkbox']:checked {
display: none;
}
input[type='checkbox']:checked + label::before {
content: '\2713';
color: red;
background-color: rgba(0, 0, 0,.01);
text-align: center;
display: inline-block;
width: 1.5rem;
margin-right: .5rem;
border: 1px solid rgba(0, 0, 0,.3);
font-weight: 900;
}
label {
margin-left: 0.5rem;
}
input[type='checkbox']:checked + label {
margin-left: 0;
text-decoration: line-through;
}
A gombokkal kapcsolatos design.
button {
border: none;
background: none;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
padding: 0.5rem 1.5rem;
border-radius: 3px;
}
button:not([type=submit]) {
margin-top: 0.3rem;
}
.add {
background-color: rgba(0, 0, 0,.1);
color: #fff;
font-size: 1.5rem;
}
.add:hover {
background-color: royalblue;
}
.hide-completed:hover, .clear:hover {
background-color: rgba(0, 0, 0,.15);
color: #fff;
}
.todo__footer {
display: flex;
justify-content: center;
}
Néhány JS mozgatással kapcsolatos CSS.
.hide {
display: none;
}
.checked {
background-color: #fff;
}
A pihenővel (.chill), üres feladatokkal kapcsolatos osztályok.
.chilldesign {
flex: 1;
text-align: center;
font-weight: 900;
color: #000000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.emoji {
font-size: 5rem;
margin: 1rem;
}
Írjuk fel először a DOM elemeket, melyekre hivatkozni fogunk, melyekkel feladatokat fogunk végezni, és beállíjuk az elvégzendő feladatok számlálójót 0-ra, valamint az összes feladatot tartalmazó tömböt is üres tömbre.
const day = document.querySelector('.day');
const date = document.querySelector('.date');
const input = document.querySelector('.input');
const inputBox = document.querySelector('.input-box');
const pendingCount = document.querySelector('.pending-count');
const pendingTodosBox = document.querySelector('.pending-todos-box');
const completedTodosBox = document.querySelector('.completed-todos-box');
const clearButton = document.querySelector('.clear');
const hideButton = document.querySelector('.hide-completed');
const chill = document.querySelector('.chill');
let count = 0;
let todos = [];
A dátumokra a toLocaleDateString-gel találunk megoldást, egy objektumban elkérhetjük a csak minket érdeklő dolgokat, jelen esetben a nap hosszú leírását és a másiknál pedig az amerikai formátum kis átalakítással megfelelő lesz.
day.textContent = new Date().toLocaleDateString('en', { weekday: 'long' });
date.textContent = new Date().toLocaleDateString('en-US').replaceAll('/', '-');
Kezdhetjük a feladatot azzal, hogy a formból, illetve annak inputjából érkező feladatot először beírjuk a localStorage-ba. Itt sokféle ellenőrzést végezhetnénk regex-ekkel, trim-mellésel, hogy ne legyen benn space vagy akármi nem kívánatos, most csak egy olyan feltételt írtam, hogy ne legyen üres, vagyis ha üresen nyomtuk meg az ENTERT (vagy a plusz gombot), akkor az ne menjen az adatbázisba. Az adatok, feladatok egyediségét sokféleképpen lehetne meghatározni, különféle véletlenszám előállításokkal, itt most az 1970-től számított ms-okat használjuk. A feladat (todo) objektumunk az egyedi id mellett tartalmazza még a feladat nevét(a bevitt szöveget) valamint, hogy elvégzendő vagy már elvégzett. Az adatbázisból aztán létrehozzuk az elemeket, kitöröljük az inputmezőt. Először tehát tegyünk a formra egy eseményfigyelőt, majd aztán adatbázis és utána elem megjelenítés.
inputBox.addEventListener('submit', (event) => {
event.preventDefault();
addTodos(input.value);
})
function addTodos(inputdata) {
if (inputdata !== '') {
const todo = {
id: Date.now(),
name: inputdata,
completed: false
}
todos.push(todo);
handleCount(todos);
addToLocalStorage(todos);
input.value = '';
pendingTodosBox.firstChild.classList.add('show-in');
}
}
function addToLocalStorage(todos) {
localStorage.setItem('todos', JSON.stringify(todos));
createTodosElement(todos);
}
Az elem megjelenítés folyamata először a meglévő elemek kiradírozása, aztán az adatbázisból megkapott adatok alapján a HTML elemek megrajzolása, kiválasztva közben, hogy a feladat már elvégzett vagy elvégzendő és a végén kiszámoljuk és megjelenítjük, hogy mennyi feladat van már elvégezve. A feladatok HTML ragasztásánál most az append helyett a prepend-et használjuk, hogy felülre kerüljön mindig a bevitt új elem.
function createTodosElement(todosArray) {
pendingTodosBox.innerHTML = '';
completedTodosBox.innerHTML = '';
todosArray.forEach(todo => {
const checked = todo.completed ? 'checked' : null;
const TodosBoxElement = document.createElement("div");
TodosBoxElement.classList.add('todo');
TodosBoxElement.setAttribute('data-id', todo.id);
if (todo.completed) { TodosBoxElement.classList.add('checked') }
TodosBoxElement.innerHTML = `
<div class="input-label-box">
<input type="checkbox" ${checked}>
<label>${todo.name}</label>
</div>
<span class="trash"></span>
`;
todo.completed ? completedTodosBox.prepend(TodosBoxElement) : pendingTodosBox.prepend(TodosBoxElement);
});
createCompletedTitle();
}
function createCompletedTitle() {
const completedTitle = document.createElement('p');
completedTitle.textContent = `Completed tasks: ${Math.round((todos.length - count) * 100 / todos.length)}%`
completedTodosBox.prepend(completedTitle);
}
A clear gombra teszünk egy eseményfigyelőt, melynek a belső függvényében az elvégzett feladatokat válogatjuk le és tesszük a localStorage-ba, ezzel átételesen kitöröljük a még nem elvégzett feladatokat.
clearButton.addEventListener('click', () => {
clearPendingElements();
})
function clearPendingElements() {
todos = todos.filter(item => {
return item.completed === true;
});
handleCount(todos);
addToLocalStorage(todos);
}
A show/hide gomra is rakunk egy eseményfigyelőt, ez a szokásos hide class mozgatás, mivel click eseményről van szó, a remove/add páros helyett használható a classList.toggle is.
hideButton.addEventListener('click', () => {
completedTodosBox.classList.toggle('hide');
completedTodosBox.classList.contains('hide') ? hideButton.textContent = 'Show Complete' : hideButton.textContent = 'Hide Complete';
})
A checkboxra és a kukára egy eseményfigyelőt teszünk és pedig az elvégzendő feladatok div-re, mert a feladatra nem tehetünk, mert amennyiben nincs semmilyen feladat, akkor az ahhoz kapcsolódó DOM elemet nem tudná felírni a JS és megakadna. A kuka törlést felírtam a már elvégzett feladatokra is. A checbox bepipálás esetén megváltoztatjuk a feladat státuszát elvégzettre, a kuka esetén pedig töröljük. A megjelölt feladatot a data-id-n keresztül párosítjuk az adatbázisban található párjához.
pendingTodosBox.addEventListener('click', (event) => {
if (event.target.type === 'checkbox') {
changeCompleted(parseInt(event.target.parentElement.parentElement.getAttribute('data-id')))
}
if (event.target.classList.contains('trash')) {
deleteTodo(parseInt(event.target.parentElement.getAttribute('data-id')));
}
})
completedTodosBox.addEventListener('click', (event) => {
if (event.target.classList.contains('trash')) {
deleteTodo(parseInt(event.target.parentElement.getAttribute('data-id')));
}
})
A két belső függvény.
function changeCompleted(id) {
todos.forEach(item => {
if (item.id == id) {
item.completed = !item.completed;
}
});
handleCount(todos);
addToLocalStorage(todos);
}
function deleteTodo(id) {
todos = todos.filter(item => {
return item.id !== id;
})
handleCount(todos);
addToLocalStorage(todos);
}
A handleCount() figyeli, hogy az elvégzendő feladatok nem ürültek-e ki, mert akkor mehetünk chill állapotba. Ha viszont van elvégzendő feladat, akkor az esetleges korábbi chill állapotot töröljük.
function handleCount(todos) {
count = todos.filter(item => item.completed === false).length;
if (count === 0) {
timeToChill();
pendingCount.parentElement.classList.add('hide');
} else {
pendingCount.parentElement.classList.remove('hide');
chill.textContent = '';
chill.classList.remove('chilldesign');
pendingCount.textContent = count;
clearButton.classList.remove('hide');
hideButton.classList.remove('hide');
}
}
A timeToChill() elrejti az elvégzett feladatokat, berakunk egy emojit és töröljük alulról a két gombot is.
function timeToChill() {
completedTodosBox.classList.add('hide');
hideButton.textContent = 'Show Complete';
chill.innerHTML = `<span class="emoji">🍻</span>
Time to chill! You have no todos.`;
chill.classList.add('chilldesign');
clearButton.classList.add('hide');
hideButton.classList.add('hide');
}
A legvégén megoldjuk az első indítás kérdését, amikor is ha már van valami a localStorage-ban, akkor azzal feltöltjük a todo list-ünket. Ezt érdemes rögtön a dátumok után tenni a kódba.
getFromLocalStorage();
function getFromLocalStorage() {
todos = JSON.parse(localStorage.getItem('todos')) || [];
handleCount(todos);
createTodosElement(todos);
}
Itt a vége: TIME TO CHILL!!!!
A teljes kód egyben a GitHub-on.