Доброго времени суток, друзья!
Сегодня мы с вами, как следует из названия, напишем полнофункциональное приложение для формирования и хранения заметок.
Возможности нашего приложения будут следующими:
- Создание заметки.
- Хранение заметок.
- Удаление заметки.
- Отметка о выполнении задачи.
- Информация о дате выполнения задачи.
- Напоминание о необходимости выполнения задачи.
Приложение будет написано на 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>
Замечания:
- Поля для ввода можно было создать с помощью тегов «figure» и «figcaption». Это было бы так сказать «семантичнее».
- Как впоследствии оказалось, выбор тега «input» с типом «date», был не лучшим решением. Об этом ниже.
- В одном из приложений напоминания (уведомления) реализованы с помощью Notifications API. Однако мне показалось странным запрашивать у пользователя разрешение на показ уведомлений и добавлять возможность их отключения, поскольку, во-первых, когда мы говорим о приложении для заметок (задач), напоминания подразумеваются, во-вторых, их можно реализовать так, чтобы они не раздражали пользователя при многократном появлении, т.е. ненавязчиво.
- Изначально в приложении предусматривалась возможность указывать не только дату, но и время напоминания. Впоследствии я решил, что даты достаточно. Впрочем, при желании ее легко добавить.
* { 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/
Добавить комментарий