Écrire une application de prise de notes JavaScript



Bonjour mes amis!

Aujourd'hui, comme son nom l'indique, nous allons écrire une application simple pour créer et stocker des notes.

Les fonctionnalités de notre application seront les suivantes:

  1. Créez une note.
  2. Stockage des notes.
  3. Supprimez une note.
  4. Marquez la tâche.
  5. Informations sur la date de fin de la tâche.
  6. Rappel pour terminer une tâche.

L'application sera écrite en JavaScript.

Les notes seront stockées dans une base de données indexée (IndexedDB). Cette bibliothèque sera utilisée pour faciliter le travail avec IndexedDB . Selon les développeurs de cette bibliothèque, c'est "la même chose que IndexedDB, mais avec des promesses".

On suppose que vous connaissez les bases d'IndexedDB. Sinon, je vous recommande de lire cet article avant de continuer .

Je comprends que pour résoudre un problème tel que le stockage de notes, LocalStorage suffit. Cependant, je voulais explorer certaines des fonctionnalités d'IndexedDB. Ainsi, le choix en faveur de ce dernier a été fait uniquement à partir de considérations épistémologiques. À la fin, vous trouverez des liens vers une application similaire où le stockage de données est implémenté à l'aide de LocalStorage.

Alors allons-y.

Notre balisage ressemble à ceci:

<!-- 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>

Remarques:

  1. Les champs de saisie peuvent être créés à l'aide des balises «figure» et «figcaption». Ce serait plus sémantique, pour ainsi dire.
  2. Comme il s'est avéré plus tard, choisir la balise "input" avec le type "date" n'était pas la meilleure solution. À ce sujet ci-dessous.
  3. Dans une application, des rappels (notifications) sont implémentés à l'aide de l'API Notifications. Cependant, il m'a semblé étrange de demander à l'utilisateur la permission d'afficher les notifications et d'ajouter la possibilité de les désactiver, car, d'une part, lorsque nous parlons de l'application pour les notes (tâches), les rappels sont implicites, et d'autre part, ils peuvent être implémentés afin de ne pas gêner l'utilisateur lors d'apparitions répétées, c'est-à-dire discrètement.
  4. Au départ, l'application permettait d'indiquer non seulement la date, mais aussi l'heure du rappel. Par la suite, j'ai décidé que la date était suffisante. Cependant, si vous le souhaitez, il est facile à ajouter.

Nous connectons les styles:
* {
    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;
}


Ne leur prêtez pas encore beaucoup d'attention.

Nous passons au script.

Recherchez les champs de saisie et créez un conteneur pour les notes:

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)

Créez une base de données et un stockage:

let db;
// IIFE
(async () => {
    //   
    // , ...
    db = await idb.openDb('db', 1, db => {
        //  
        db.createObjectStore('notes', {
            keyPath: 'id'
        })
    })

    //  
    createList()
})();

Considérez la fonction d'ajouter des notes afin de comprendre ce qu'est une seule note ou, plus précisément, ce qu'elle contient. Cela vous aidera à comprendre comment la liste est formée:

//         ""
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 { }
}

Maintenant, nous allons créer une liste:

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

Un tableau d'objets pour stocker les dates de rappel a deux champs: "id" pour identifier les notes et "date" pour comparer les dates. En écrivant la valeur de la date de rappel dans le champ "date", nous sommes obligés de convertir cette valeur, car inputDate.value renvoie des données au format "aaaa-mm-jj", et nous allons comparer ces données avec les données au format auquel nous sommes habitués, c'est-à-dire "Jj.mm.aaaa". Par conséquent, nous utilisons la méthode «replace» et l'expression régulière, où, en utilisant le regroupement, nous inversons les blocs et remplaçons les tirets par des points. Il peut y avoir une solution plus polyvalente ou élégante.

Ensuite, nous travaillons avec des notes:

       // ...
       //          ""
       //       
       //     /   
       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)
}

La fonction pour supprimer une note de la liste et du stockage ressemble à ceci:

const deleteNote = async key => {
    //        ()
    await db.transaction('notes', 'readwrite')
        .objectStore('notes')
        .delete(key)
    await createList()
}

Notre application n'a pas la possibilité de supprimer la base de données, mais la fonction correspondante pourrait ressembler à ceci:

document.querySelector('.delete-btn').onclick = async () => {
    //   
    await idb.deleteDb('dataBase')
        //  
        .then(location.reload())
}

La fonction de vérification des rappels compare la date actuelle et les dates de rappel saisies par l'utilisateur:

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 = '!'
        }
    })
}

Enfin, ajoutez un gestionnaire d'erreurs à l'objet Window qui n'a pas été traité dans les blocs de code correspondants:

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

Le résultat ressemble à ceci:



→ Code sur Github

Voici une application similaire sur le stockage local:



→ Le code de cette application sur Github

je serai heureux de tout commentaire.

Merci de votre attention.

All Articles