Пишем полнофункциональное приложение для заметок на JavaScript

от автора

Доброго времени суток, друзья!

Сегодня мы с вами, как следует из названия, напишем полнофункциональное приложение для формирования и хранения заметок.

Возможности нашего приложения будут следующими:

  1. Создание заметки.
  2. Хранение заметок.
  3. Удаление заметки.
  4. Отметка о выполнении задачи.
  5. Информация о дате выполнения задачи.
  6. Напоминание о необходимости выполнения задачи.

Приложение будет написано на JavaScript.

Заметки будут храниться в индексированной базе данных (IndexedDB). Для облегчения работы с IndexedDB будет использована эта библиотека. Как заявляют разработчики данной библиотеки, она представляет собой «тоже самое, что и IndexedDB, но с промисами».

Предполагается, что вы знакомы с азами IndexedDB. Если нет, то прежде чем продолжить рекомендую прочитать эту статью.

Я понимаю, что для решения такой задачи, как хранение заметок, вполне достаточно LocalStorage. Однако, мне хотелось исследовать некоторые возможности IndexedDB. Таким образом, выбор в пользу последней был сделан исключительно из гносеологических соображений. В конце будут приведены ссылки на похожее приложение, где хранение данных реализовано с помощью LocalStorage.

Итак, поехали.

Наша разметка выглядит так:

<!-- head --> <!-- шрифт --> <link href="https://fonts.googleapis.com/css2?family=Stylish&display=swap" rel="stylesheet"> <!-- библиотека --> <script src="https://cdn.jsdelivr.net/npm/idb@3.0.2/build/idb.min.js"></script>  <!-- body --> <!-- основной контейнер --> <div class="box">     <!-- изображение-заполнитель -->     <img src="https://placeimg.com/480/240/nature" alt="#">     <!-- поле для ввода текста заметки -->     <p>Note text: </p>     <textarea></textarea>     <!-- поле для ввода даты напоминания -->     <p>Notification date: </p>     <input type="date">      <!-- кнопка для добавления заметки -->     <button class="add-btn">add note</button>     <!-- кнопка для очистки хранилища -->     <button class="clear-btn">clear storage</button> </div> 

Замечания:

  1. Поля для ввода можно было создать с помощью тегов «figure» и «figcaption». Это было бы так сказать «семантичнее».
  2. Как впоследствии оказалось, выбор тега «input» с типом «date», был не лучшим решением. Об этом ниже.
  3. В одном из приложений напоминания (уведомления) реализованы с помощью Notifications API. Однако мне показалось странным запрашивать у пользователя разрешение на показ уведомлений и добавлять возможность их отключения, поскольку, во-первых, когда мы говорим о приложении для заметок (задач), напоминания подразумеваются, во-вторых, их можно реализовать так, чтобы они не раздражали пользователя при многократном появлении, т.е. ненавязчиво.
  4. Изначально в приложении предусматривалась возможность указывать не только дату, но и время напоминания. Впоследствии я решил, что даты достаточно. Впрочем, при желании ее легко добавить.

Подключаем стили:

* {     margin: 0;     padding: 0;     box-sizing: border-box; }  body {     height: 100vh;     background: radial-gradient(circle, skyblue, steelblue);     display: flex;     flex-wrap: wrap;     justify-content: center;     align-items: center;     font-family: 'Stylish', sans-serif;     font-size: 1.2em; }  .box, .list {     margin: 0 .4em;     width: 320px;     display: flex;     flex-direction: column;     justify-content: center;     align-items: center;     background: linear-gradient(lightyellow, darkorange);     border-radius: 5px;     padding: .6em;     box-shadow: 0 0 4px rgba(0, 0, 0, .6) }  img {     padding: .4em;     width: 100%; }  h3 {     user-select: none; }  p {     margin: .2em 0;     font-size: 1.1em; }  textarea {     width: 300px;     height: 80px;     padding: .4em;     border-radius: 5px;     font-size: 1em;     resize: none;     margin-bottom: .7em; }  input[type="date"] {     width: 150px;     text-align: center;     margin-bottom: 3em; }  button {     width: 140px;     padding: .4em;     margin: .4em 0;     cursor: pointer;     border: none;     background: linear-gradient(lightgreen, darkgreen);     border-radius: 5px;     font-family: inherit;     font-size: .8em;     text-transform: uppercase;     box-shadow: 0 2px 2px rgba(0, 0, 0, .5); }  button:active {     box-shadow: 0 1px 1px rgba(0, 0, 0, .7); }  button:focus, textarea:focus, input:focus {     outline: none; }  .note {     display: flex;     flex-wrap: wrap;     justify-content: center;     align-items: center;     font-style: italic;     user-select: none;     word-break: break-all;     position: relative; }  .note p {     width: 240px;     font-size: 1em; }  .note span {     display: block;     cursor: pointer;     font-weight: bold;     font-style: normal; }  .info {     color: blue; }  .notify {     color: #ddd;     font-size: .9em;     font-weight: normal !important;     text-align: center;     line-height: 25px;     border-radius: 5px;     width: 130px;     height: 25px;     position: absolute;     top: -10px;     left: -65px;     background: rgba(0, 0, 0, .6);     transition: .2s;     opacity: 0; }  .show {     opacity: 1; }  .info.null, .notify.null {     display: none; }  .complete {     padding: 0 .4em;     color: green; }  .delete {     padding-left: .4em;     color: red; }  .line-through {     text-decoration: line-through; } 

Пока не обращайте на них много внимания.

Переходим к скрипту.

Находим поля для ввода и создаем контейнер для заметок:

let textarea = document.querySelector('textarea') let dateInput = document.querySelector('input[type="date"]')  let list = document.createElement('div') list.classList.add('list') document.body.appendChild(list) 

Создаем базу данных и хранилище:

let db; // IIFE (async () => {     // создаем базу данных     // название, версия...     db = await idb.openDb('db', 1, db => {         // создаем хранилище         db.createObjectStore('notes', {             keyPath: 'id'         })     })      // формируем список     createList() })(); 

Рассмотрим функцию добавления заметки, чтобы понимать, что из себя представляет или, точнее, что содержит одна заметка. Это поможет понять, как формируется список:

// добавляем к кнопке для добавления заметки обработчик события "клик" document.querySelector('.add-btn').onclick = addNote  const addNote = async () => {     // если поле для ввода текста пустое, ничего не делаем     if (textarea.value === '') return      // получаем значение этого поля     let text = textarea.value      // объявляем переменную для даты напоминания     // с помощью тернарного оператора     // присваиваем этой переменной null или значение соответствующего поля     let date     dateInput.value === '' ? date = null : date = dateInput.value      // заметка представляет собой объект     let note = {         id: id,         text: text,         // дата создания         createdDate: new Date().toLocaleDateString(),         // индикатор выполнения         completed: '',         // дата напоминания         notifyDate: date     }      // пробуем записать данные в хранилище     try {         await db.transaction('notes', 'readwrite')             .objectStore('notes')             .add(note)         // формируем список         await createList()             // обнуляем значения полей             .then(() => {                 textarea.value = ''                 dateInput.value = ''             })     } catch { } } 

Теперь займемся формированием списка:

let id  const createList = async () => {     // добавляем заголовок     // дату формируем с помощью API интернационализации     list.innerHTML = `<h3>Today is ${new Intl.DateTimeFormat('en', { year: 'numeric', month: 'long', day: 'numeric' }).format()}</h3>`      // получаем заметки из базы данных     let notes = await db.transaction('notes')         .objectStore('notes')         .getAll()      // массив для дат напоминаний     let dates = []      // если в базе имеются данные     if (notes.length) {         // присваиваем переменной "id" номер последней заметки         id = notes.length          // итерация по массиву         notes.map(note => {            // добавляем заметки в список             list.insertAdjacentHTML('beforeend',             // добавляем заметке атрибут "data-id"             `<div class = "note" data-id="${note.id}">             // дата уведомления             <span class="notify ${note.notifyDate}">${note.notifyDate}</span>             // значок (кнопка) отображения уведомления             // обратите внимание, что в качестве дополнительного класса             // мы добавляем тексту и значку уведомления дату напоминания             // если дата не указана             // текст и значок уведомления не отображаются (CSS: .info.null, .notify.null)             <span class="info ${note.notifyDate}">?</span>              // значок (кнопка) выполнения задачи             <span class="complete">V</span>             // в качестве класса к тексту заметки добавляется индикатор выполнения             <p class="${note.completed}">Text: ${note.text}, <br> created: ${note.createdDate}</p>             // значок (кнопка) удаления заметки             <span class="delete">X</span>         </div>`)             // заполняем массив с датами напоминаний             // если дата не указана             if (note.notifyDate === null) {                 return             // если дата указана             } else {                 // массив объектов                 dates.push({                     id: note.id,                     date: note.notifyDate.replace(/(\d+)-(\d+)-(\d+)/, '$3.$2.$1')                 })             }         })     // если в базе не имеется данных     } else {         // присваиваем переменной "id" значение 0         id = 0          // выводим в список текст об отсутствии заметок         list.insertAdjacentHTML('beforeend', '<p class="note">empty</p>')     }     // ...to be continued 

Массив объектов для хранения дат напоминаний имеет два поля: «id» для идентификации заметки и «date» для сравнения дат. Записывая значение даты напоминания в поле «date», мы вынуждены это значение преобразовывать, поскольку inputDate.value возвращает данные в формате «гггг-мм-дд», а мы собираемся сравнивать эти данные с данными в привычном нам формате, т.е. «дд.мм.гггг». Поэтому мы используем метод «replace» и регулярное выражение, где с помощью группировки инвертируем блоки и заменяем дефисы точками. Возможно, существует более универсальное или элегантное решение.

Далее работаем с заметками:

       // ...        // находим все заметки и добавляем к каждой обработчик события "клик"        // мы делаем это внутри функции формирования списка        // поскольку наш список при добавлении/удалении заметки формируется заново        document.querySelectorAll('.note').forEach(note => note.addEventListener('click', event => {         // если целью клика является элемент с классом "complete" (кнопка выполнения задачи)         if (event.target.classList.contains('complete')) {             // добавляем/удаляем у следующего элемента (текст заметки) класс "line-through", отвечающий за зачеркивание текста             event.target.nextElementSibling.classList.toggle('line-through')              // меняем значение индикатора выполнения заметки             // в зависимости от наличия класса "complete"             note.querySelector('p').classList.contains('line-through')                 ? notes[note.dataset.id].completed = 'line-through'                 : notes[note.dataset.id].completed = ''              // перезаписываем заметку в хранилище             db.transaction('notes', 'readwrite')                 .objectStore('notes')                 .put(notes[note.dataset.id])          // если целью клика является элемент с классом "delete" (кнопка удаления заметки)         } else if (event.target.classList.contains('delete')) {             // вызываем соответствующую функцию со значением идентификатора заметки в качестве параметра             // обратите внимание, что нам необходимо преобразовать id в число             deleteNote(+note.dataset.id)          // если целью клика является элемент с классом "info" (кнопка отображения даты напоминания)         } else if (event.target.classList.contains('info')) {             // добавляем/удаляем у предыдущего элемента (дата напоминания) класс "show", отвечающий за отображение             event.target.previousElementSibling.classList.toggle('show')         }     }))      // запускаем проверку напоминаний     checkDeadline(dates) } 

Функция удаления заметки из списка и хранилища выглядит так:

const deleteNote = async key => {     // открываем транзакцию и удаляем заметку по ключу (идентификатор)     await db.transaction('notes', 'readwrite')         .objectStore('notes')         .delete(key)     await createList() } 

В нашем приложении отсутствует возможность удаления базы данных, но соответствующая функция могла бы выглядеть следующим образом:

document.querySelector('.delete-btn').onclick = async () => {     // удаляем базу данных     await idb.deleteDb('dataBase')         // перезагружаем страницу         .then(location.reload()) } 

Функция проверки напоминаний сравнивает текущую дату и даты напоминаний, введенные пользователем:

const checkDeadline = async dates => {     // получаем текущую дату в формате "дд.мм.гггг"     let today = `${new Date().toLocaleDateString()}`      // итерация по массиву     dates.forEach(date => {         // если текущая дата и одна из дат напоминаний совпадают         if (date.date === today) {             // меняем кнопку отображения напоминания с "?" на "!"             document.querySelector(`div[data-id="${date.id}"] .info`).textContent = '!'         }     }) } 

В завершение добавляем к объекту Window обработчик ошибок, которые не были обработаны в соответствующих блоках кода:

window.addEventListener('unhandledrejection', event => {     console.error('error: ' + event.reason.message) }) 

Результат выглядит так:

Код на Github.

Вот похожее приложение на Local Storage:

Код этого приложения на Github.

Буду рад любым замечаниям.

Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/500312/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *