Hello I've created to do app with some additional functionality.
Since I am new in JS I would love some feedback if you guys could point out some obvious mistakes/good practices/better solutions or implementations I will be grateful.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style/main.css" />
<title>To do app</title>
</head>
<body>
<div class="container">
<div class="banner">
<h2>Welcome</h2>
<div class="timestamp">
<div class="timestamp__time">12:20</div>
<div class="timestamp__date">Tuesday, 5 January</div>
</div>
</div>
<div class="sidebar">
<h3>Your lists</h3>
<div class="lists">
<div class="list active">
<svg class="list__icon">
<use xlink:href="img/icons/sprite.svg#icon-bookmark"></use>
</svg>
<input class="list__title" value="Important" readonly />
<div class="list__options">
<div class="option"></div>
<div class="option"></div>
</div>
</div>
<div class="list">
<svg class="list__icon">
<use xlink:href="img/icons/sprite.svg#icon-light-bulb"></use>
</svg>
<input class="list__title" value="Ideas" readonly />
<div class="list__options">
<div class="option"></div>
<div class="option"></div>
</div>
</div>
<div class="list">
<svg class="list__icon">
<use xlink:href="img/icons/sprite.svg#icon-shopping-basket"></use>
</svg>
<input class="list__title" value="Groceries" readonly />
<div class="list__options">
<div class="option"></div>
<div class="option"></div>
</div>
</div>
</div>
<div class="add-list-container">
<div class="add-list">
<img src="img/plus.svg" alt="plus sign" />
<p>New list</p>
</div>
</div>
</div>
<main class="main-section">
<div class="main">
<div class="todo show" id="Important">
<div class="item">
<div class="item__wrapper">
<div class="item__check item__check--done">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title item__title--done" value="Walk the dog" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename" />
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>
</div>
<div class="item">
<div class="item__wrapper">
<div class="item__check">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title" value="Finish essay" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename" />
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>
</div>
</div>
<div class="todo" id="Ideas">
<div class="item">
<div class="item__wrapper">
<div class="item__check">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title" value="Weight loss tracker" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename" />
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>
</div>
<div class="item">
<div class="item__wrapper">
<div class="item__check">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title" value="Meal planning" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename" />
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>
</div>
</div>
<div class="todo" id="Groceries">
<div class="item">
<div class="item__wrapper">
<div class="item__check">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title" value="Milk" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename" />
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>
</div>
<div class="item">
<div class="item__wrapper">
<div class="item__check">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title" value="Broccoli" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename" />
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>
</div>
</div>
</div>
<div class="add-item">
<img src="img/plus.svg" alt="plus sign" />
<p>New item</p>
</div>
</main>
</div>
<script src="script.js"></script>
</body>
</html>
CSS (generated by SAS)
@import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600&display=swap");
*,
*::after,
*::before {
margin: 0;
padding: 0;
text-decoration: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-family: 'Quicksand', sans-serif;
}
html {
font-size: 62.5%;
}
body {
height: 100vh;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.container {
display: -ms-grid;
display: grid;
width: 100%;
height: 100%;
grid-template-areas: 'banner banner banner'
'sidebar main main'
'sidebar main main';
-ms-grid-columns: 35rem 1fr 1fr;
grid-template-columns: 35rem 1fr 1fr;
-ms-grid-rows: 23rem 1fr 1fr;
grid-template-rows: 23rem 1fr 1fr;
}
.container .main-section {
-ms-grid-row: 2;
-ms-grid-row-span: 2;
-ms-grid-column: 2;
-ms-grid-column-span: 2;
grid-area: main;
}
.banner {
background-image: url(../img/banner.svg);
background-repeat: no-repeat;
background-size: cover;
background-position: bottom;
-ms-grid-row: 1;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
grid-area: banner;
color: #fff;
padding: 3rem 5rem;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.banner .timestamp__time {
font-size: 7rem;
font-weight: 300;
}
.banner .timestamp__date {
font-weight: 500;
font-size: 2rem;
}
.banner h2 {
font-size: 3rem;
font-weight: 600;
}
.sidebar {
background-color: #f5f5f5;
-ms-grid-row: 2;
-ms-grid-row-span: 2;
-ms-grid-column: 1;
grid-area: sidebar;
}
.sidebar h3 {
margin-bottom: 4rem;
margin-top: 5rem;
font-size: 2.5rem;
font-weight: 600;
padding: 0 5rem;
}
.sidebar .list {
display: -ms-grid;
display: grid;
-ms-grid-columns: (1fr)[3];
grid-template-columns: repeat(3, 1fr);
padding: 2rem 5rem;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
position: relative;
}
.sidebar .list__icon {
width: 2.2rem;
height: 2.2rem;
fill: currentColor;
}
.sidebar .list:hover {
background-color: #fff;
cursor: pointer;
}
.sidebar .list__title {
font-size: 2rem;
margin-left: 0.5rem;
background: none;
border: none;
color: #000;
outline: none;
display: inline-block;
cursor: pointer;
}
.sidebar .list .icon-modal {
border-radius: 3px;
padding: 3rem;
margin-left: 5rem;
position: absolute;
background-color: #fff;
bottom: 100%;
width: 100%;
}
.sidebar .list .icon-modal > * {
margin: 0.7rem;
fill: black;
cursor: pointer;
}
.sidebar .list .icon-modal::after {
position: absolute;
content: '';
left: 0;
top: 100%;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 15px solid #fff;
}
.sidebar .list__options {
position: relative;
padding: 0.5rem;
}
.sidebar .list__options .list-modal {
position: absolute;
background-color: white;
color: black;
bottom: 100%;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
width: 10rem;
font-weight: 600;
font-size: 1.2rem;
margin-bottom: 1rem;
}
.sidebar .list__options .list-modal p {
padding: 0.5rem 1rem;
}
.sidebar .list__options .list-modal p:hover {
cursor: pointer;
background-color: #ff3366;
color: #fff;
}
.sidebar .list .icon-modal-close {
width: 2rem;
height: 2rem;
position: absolute;
top: 5px;
right: 5px;
}
.sidebar .list .option {
background-color: black;
width: 0.6rem;
border-radius: 50%;
height: 0.6rem;
}
.sidebar .list .option:first-child {
margin-bottom: 0.6rem;
}
.sidebar .add-list-container {
padding: 2rem 5rem;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 2rem;
}
.sidebar .add-list-container .add-list {
cursor: pointer;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-item-align: start;
align-self: flex-start;
}
.sidebar .add-list-container .add-list img {
width: 3rem;
}
.todo {
font-size: 5rem;
display: none;
}
.item {
padding: 3rem 5rem;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-shadow: 0 2px 2px -2px gray;
box-shadow: 0 2px 2px -2px gray;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
}
.item__wrapper {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.item__check {
width: 2.5rem;
height: 2.5rem;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
border-radius: 50%;
border: 1px solid black;
cursor: pointer;
}
.item__check--done {
background-color: #40d628;
border: none;
}
.item__check img {
width: 1.5rem;
height: 1.5rem;
}
.item__title {
font-size: 2rem;
margin-left: 1rem;
background: none;
border: none;
color: #000;
outline: none;
display: inline-block;
}
.item__title--done {
text-decoration: line-through;
}
.item__options {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.item__options img {
cursor: pointer;
width: 2.5rem;
height: 2.5rem;
}
.item__options img:not(:first-child) {
margin-left: 2rem;
}
.add-item {
padding: 3rem 5rem;
-webkit-box-shadow: 0 2px 2px -2px gray;
box-shadow: 0 2px 2px -2px gray;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
font-size: 2rem;
cursor: pointer;
}
.add-item img {
width: 3rem;
}
.active {
background-color: #ff3366;
color: #fff;
}
.active .list__title {
color: white !important;
cursor: auto;
}
.active .list__icon {
fill: currentColor;
}
.active:hover {
background-color: #ff3366 !important;
cursor: auto !important;
}
.active .option {
background-color: white !important;
}
.show {
display: block;
}
.hide {
display: none !important;
}
/*# sourceMappingURL=main.css.map */
JS
'use strict';
// #######################################
// DATE & TIME
const timeBox = document.querySelector('.timestamp__time');
const dateBox = document.querySelector('.timestamp__date');
let now, hours, minutes;
const clock = () => {
now = new Date();
hours = now.getHours();
minutes = now.getMinutes();
hours < 10 ? (hours = `0${hours}`) : hours;
minutes < 10 ? (minutes = `0${minutes}`) : minutes;
timeBox.textContent = `${hours}:${minutes}`;
};
setInterval(clock, 1000);
const setDate = () => {
now = new Date();
let weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
let months = ['January', 'February', 'March', 'April', 'May', 'Juny', 'July', 'August', 'September', 'October', 'November', 'December'];
// now.getDay() - returns 0-6
let weekDay = weekDays[now.getDay()];
let day = now.getDate();
let month = months[now.getMonth()];
dateBox.textContent = `${weekDay}, ${day} ${month}`;
};
setDate();
// #######################################
// SWITCHING BETWEEN LISTS
const listsContainer = document.querySelector('.lists');
let lists = document.querySelectorAll('.list');
let activeList = lists[0];
// Add eventListener to lists
const setUpListsListeners = () => {
lists = document.querySelectorAll('.list');
for (let i = 0; i < lists.length; i++) {
lists[i].addEventListener('click', () => {
if (!lists[i]) return;
if (!lists[i].classList.contains('active')) switchList(lists[i]); // listener only for non active lists
});
}
};
// call to setUp 3 primary lists
setUpListsListeners();
const switchList = (list) => {
if (typeof activeList !== 'undefined') {
// for more than 1 list
// 'turn off' active list
activeList.classList.toggle('active');
document.querySelector(`#${activeList.children[1].value}`).classList.toggle('show');
}
// switch list by 'turning on' list from param
activeList = list;
activeList.classList.toggle('active');
document.querySelector(`#${activeList.children[1].value}`).classList.toggle('show'); // display to'do items for this list
};
// #######################################
// MODAL
// parent in this case is a list in which we click settings - prevents opening multiple modals
let optionsParent;
const openModal = (parent) => {
// create 3 different elements for each event - icon change, rename, delete
const modal = document.createElement('div');
modal.className = 'list-modal';
const rename = document.createElement('p');
rename.className = 'rename';
rename.textContent = 'Rename';
modal.appendChild(rename);
const changeIcon = document.createElement('p');
changeIcon.className = 'change-icon';
changeIcon.textContent = 'Change icon';
modal.appendChild(changeIcon);
const deleteList = document.createElement('p');
deleteList.className = 'delete';
deleteList.textContent = 'Delete';
modal.appendChild(deleteList);
parent.appendChild(modal);
optionsParent = parent;
activateModalButtons();
};
const closeModal = (parent) => {
const modal = document.querySelector('.list-modal');
if (!modal) return;
parent.removeChild(modal);
optionsParent = false;
};
// function that checks if we click in either option circular icon or it's container
// if it's already opened and we click outside it - closeModal()
// if yes then openModal()
window.addEventListener('click', function (e) {
if (optionsParent) closeModal(optionsParent);
if (e.target.classList.contains('list__options')) openModal(e.target);
if (e.target.classList.contains('option')) openModal(e.target);
});
// #######################################
// MODAL - RENAME, CHANGE ICON & DELETE
const activateModalButtons = () => {
const renameBtn = document.querySelector('.rename');
const deleteBtn = document.querySelector('.delete');
const changeIconBtn = document.querySelector('.change-icon');
const currentList = activeList.children[1];
renameBtn.addEventListener('click', () => {
renameList(currentList);
});
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
// remove lists on which we click delete button
listsContainer.removeChild(activeList);
document.querySelector(`#${activeList.children[1].value}`).classList.add('hide');
lists = document.querySelectorAll('.list');
// activate first list
activeList = lists[0];
if (!activeList) return; // return when there is no more lists
activeList.classList.toggle('active');
document.querySelector(`#${activeList.children[1].value}`).classList.toggle('show');
// refresh listeners for lists
setUpListsListeners();
});
changeIconBtn.addEventListener('click', (e) => {
e.stopPropagation();
openIconModal(activeList);
});
};
// icon change
const openIconModal = (parent) => {
const iconModal = document.createElement('div');
iconModal.className = 'icon-modal';
iconModal.innerHTML = `
<svg class="list__icon" id="icon-shopping-basket">
<use xlink:href="img/icons/sprite.svg#icon-shopping-basket"></use>
</svg>
<svg class="list__icon" id="icon-bookmark">
<use xlink:href="img/icons/sprite.svg#icon-bookmark"></use>
</svg>
<svg class="list__icon" id="icon-beamed-note">
<use xlink:href="img/icons/sprite.svg#icon-beamed-note"></use>
</svg>
<svg class="list__icon" id="icon-bowl">
<use xlink:href="img/icons/sprite.svg#icon-bowl"></use>
</svg>
<svg class="list__icon" id="icon-clipboard">
<use xlink:href="img/icons/sprite.svg#icon-clipboard"></use>
</svg>
<svg class="list__icon" id="icon-credit-card">
<use xlink:href="img/icons/sprite.svg#icon-credit-card"></use>
</svg>
<svg class="list__icon" id="icon-globe">
<use xlink:href="img/icons/sprite.svg#icon-globe"></use>
</svg>
<svg class="list__icon" id="icon-heart">
<use xlink:href="img/icons/sprite.svg#icon-heart"></use>
</svg>
<svg class="list__icon" id="icon-home">
<use xlink:href="img/icons/sprite.svg#icon-home"></use>
</svg>
<svg class="list__icon" id="icon-hour-glass">
<use xlink:href="img/icons/sprite.svg#icon-hour-glass"></use>
</svg>
<svg class="list__icon" id="icon-laptop">
<use xlink:href="img/icons/sprite.svg#icon-laptop"></use>
</svg>
<svg class="list__icon" id="icon-light-bulb">
<use xlink:href="img/icons/sprite.svg#icon-light-bulb"></use>
</svg>
<svg class="list__icon" id="icon-location-pin">
<use xlink:href="img/icons/sprite.svg#icon-location-pin"></use>
</svg>
<svg class="list__icon" id="icon-thumbs-up">
<use xlink:href="img/icons/sprite.svg#icon-thumbs-up"></use>
</svg>
<svg class="list__icon" id="icon-suitcase">
<use xlink:href="img/icons/sprite.svg#icon-suitcase"></use>
</svg>
<svg class="list__icon" id="icon-mail">
<use xlink:href="img/icons/sprite.svg#icon-mail"></use>
</svg>
<img src="img/close.svg" class="icon-modal-close">
`;
parent.appendChild(iconModal);
const iconModalCloseBtn = document.querySelector('.icon-modal-close');
let isModal = true;
iconModalCloseBtn.addEventListener('click', (e) => {
e.stopPropagation();
isModal = false;
parent.removeChild(iconModal);
});
window.addEventListener('click', () => {
if (isModal) {
parent.removeChild(iconModal);
isModal = false;
}
});
const listIcons = document.querySelectorAll('.list__icon');
for (const icon of listIcons) {
icon.addEventListener('click', () => {
activeList.children[0].children[0].setAttributeNS('http://www.w3.org/1999/xlink', 'href', `img/icons/sprite.svg#${icon.id}`);
});
}
};
const renameList = (currentList) => {
// place cursor in list name (focus input)
currentList.readOnly = false;
setCaretPosition(currentList, currentList.value.length);
// rename it on either enter key or blur
const tmpName = currentList.value;
currentList.addEventListener('blur', () => {
if (!document.querySelector(`#${tmpName}`)) return; // prevent executing both events
currentList.readOnly = true;
document.querySelector(`#${tmpName}`).id = currentList.value;
});
currentList.addEventListener('keyup', () => {
if (event.keyCode === 13) {
if (!document.querySelector(`#${tmpName}`)) return;
currentList.readOnly = true;
document.querySelector(`#${tmpName}`).id = currentList.value;
}
});
};
// #######################################
// ADD NEW LIST
const addListBtn = document.querySelector('.add-list');
const main = document.querySelector('.main');
let newListsNumber = 1;
addListBtn.addEventListener('click', () => {
// create new list
const newList = document.createElement('div');
newList.className = 'list';
newList.innerHTML = `
<svg class="list__icon">
<use xlink:href="img/icons/sprite.svg#icon-clipboard"></use>
</svg>
<input class="list__title" value="New-${newListsNumber}" readonly />
<div class="list__options">
<div class="option"></div>
<div class="option"></div>
</div>`;
listsContainer.appendChild(newList);
// create new item for current list
const newTodo = document.createElement('div');
newTodo.className = 'todo';
newTodo.id = newList.children[1].value;
main.appendChild(newTodo);
setUpListsListeners();
renameList(newList.children[1]);
newListsNumber++;
});
// #######################################
// ITEMS - DISPLAY TRASH AND RENAME ICON ON HOVER
let currentItem;
const setUpItems = () => {
const items = document.querySelectorAll('.item');
for (const item of items) {
item.addEventListener('mouseover', () => {
item.children[1].classList.remove('hide');
currentItem = item;
});
item.addEventListener('mouseout', () => {
item.children[1].classList.add('hide');
});
}
// RENAME BUTTONS
const renameItemButtons = document.querySelectorAll('.item-rename');
for (const renameBtn of renameItemButtons) {
renameBtn.addEventListener('click', () => {
const itemName = currentItem.children[0].children[1];
if (itemName.classList.contains('item__title--done')) return;
renameItem(itemName);
});
}
// DELETE BUTTONS
const deleteItemButtons = document.querySelectorAll('.item-delete');
for (const deleteBtn of deleteItemButtons) {
deleteBtn.addEventListener('click', () => {
currentItem.remove();
});
}
};
// #######################################
// MARK AS DONE
// for default items
const checkBoxes = document.querySelectorAll('.item__check');
for (const checkBox of checkBoxes) {
checkBox.addEventListener('click', () => {
checkBox.classList.toggle('item__check--done');
currentItem.children[0].children[1].classList.toggle('item__title--done');
});
}
// for new items
const markAsDoneAddListener = () => {
currentItem.children[0].children[0].classList.toggle('item__check--done');
currentItem.children[0].children[1].classList.toggle('item__title--done');
};
setUpItems();
// #######################################
// ADD ITEM
const addItemBtn = document.querySelector('.add-item');
addItemBtn.addEventListener('click', () => {
const itemParent = document.querySelector(`#${activeList.children[1].value}`);
const newItem = document.createElement('div');
newItem.className = 'item';
newItem.innerHTML = `
<div class="item__wrapper">
<div class="item__check" onClick="markAsDoneAddListener()">
<img src="img/checkmark.svg" alt="checkmark" />
</div>
<input class="item__title" value="New item" readonly />
</div>
<div class="item__options hide">
<img src="img/pen.svg" alt="pen" class="item-rename"/>
<img src="img/trash.svg" alt="trash" class="item-delete" />
</div>`;
itemParent.appendChild(newItem);
setUpItems();
renameItem(newItem.children[0].children[1]);
});
// #######################################
// RENAME ITEM
const renameItem = (itemName) => {
// place cursor in list name (focus input)
itemName.readOnly = false;
setCaretPosition(itemName, itemName.value.length);
// rename it on either enter key or blur
const tmpName = itemName.value;
itemName.addEventListener('blur', () => {
if (!document.querySelector(`#${tmpName}`)) return; // prevent executing both events
itemName.readOnly = true;
document.querySelector(`#${tmpName}`).id = itemName.value;
});
itemName.addEventListener('keyup', () => {
if (event.keyCode === 13) {
if (!document.querySelector(`#${tmpName}`)) return;
itemName.readOnly = true;
document.querySelector(`#${tmpName}`).id = itemName.value;
}
});
};
// #######################################
// SET CURSOR INSIDE INPUT
function setCaretPosition(ctrl, pos) {
// Modern browsers
if (ctrl.setSelectionRange) {
ctrl.focus();
ctrl.setSelectionRange(pos, pos);
// IE8 and below
} else if (ctrl.createTextRange) {
var range = ctrl.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}