Travailler avec des fichiers en JavaScript

Bonne journée, mes amis!



L'opinion selon laquelle JavaScript ne sait pas comment interagir avec le systÚme de fichiers n'est pas entiÚrement correcte. Il s'agit plutÎt du fait que cette interaction est considérablement limitée par rapport aux langages de programmation cÎté serveur tels que Node.js ou PHP. Néanmoins, JavaScript peut à la fois recevoir (recevoir) et créer certains types de fichiers et les traiter avec succÚs de maniÚre native.



Dans cet article, nous allons créer trois petits projets:



  • Nous implĂ©mentons la rĂ©ception et le traitement des images, audio, vidĂ©o et texte au format txt et pdf
  • CrĂ©ons un gĂ©nĂ©rateur de fichiers JSON
  • Écrivons deux programmes: l'un formera des questions (au format JSON), et l'autre les utilisera pour crĂ©er un test


Si vous ĂȘtes intĂ©ressĂ©, suivez-moi.



Code de projet sur GitHub .



Nous recevons et traitons les fichiers



Commençons par créer un répertoire dans lequel nos projets seront stockés. Appelons cela "Work-With-Files-in-JavaScript" ou ce que vous voulez.



Dans ce répertoire, nous allons créer un dossier pour le premier projet. Appelons cela "File-Reader".



Créez-y un fichier "index.html" avec le contenu suivant:



<div>+</div>
<input type="file">


Ici, nous avons un rĂ©cepteur de fichier conteneur et une entrĂ©e avec le type "fichier" (pour obtenir un fichier; nous travaillerons avec des fichiers uniques; pour obtenir plusieurs fichiers, l'entrĂ©e doit ĂȘtre ajoutĂ©e avec l'attribut "multiple"), qui sera cachĂ© sous le conteneur.



Les styles peuvent ĂȘtre inclus dans un fichier sĂ©parĂ© ou dans la balise "style" Ă  l'intĂ©rieur de l'en-tĂȘte:



body {
    margin: 0 auto;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    max-width: 768px;
    background: radial-gradient(circle, skyblue, steelblue);
    color: #222;
}

div {
    width: 150px;
    height: 150px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 10em;
    font-weight: bold;
    border: 6px solid;
    border-radius: 8px;
    user-select: none;
    cursor: pointer;
}

input {
    display: none;
}

img,
audio,
video {
    max-width: 80vw;
    max-height: 80vh;
}


Vous pouvez faire le design à votre goût.



N'oubliez pas non plus d'inclure le script dans la tĂȘte avec l'attribut "defer" (il faut attendre que le DOM soit dessinĂ© (rendu); vous pouvez bien sĂ»r le faire dans le script en gĂ©rant l'Ă©vĂ©nement "load" ou "DOMContentLoaded" de l'objet "window", mais le report est beaucoup plus court) , ou avant la balise de fermeture «body» (alors ni un attribut ni un gestionnaire ne sont nĂ©cessaires). Personnellement, je prĂ©fĂšre la premiĂšre option.



Ouvrons index.html dans un navigateur:







Avant de procéder à l'écriture du script, nous devons préparer les fichiers pour l'application: nous avons besoin d'une image, audio, vidéo, texte en txt, pdf et tout autre format, par exemple, doc. Vous pouvez utiliser ma collection ou créer la vÎtre.



Nous devons souvent accéder aux objets "document" et "document.body", ainsi que sortir les résultats plusieurs fois sur la console, donc je suggÚre d'envelopper notre code dans cet IIFE (ce n'est pas nécessaire):



;((D, B, log = arg => console.log(arg)) => {

    //  

    //     document  document.body   D  B, 
    // log = arg => console.log(arg) -      
    //    console.log  log
})(document, document.body)


Tout d'abord, nous déclarons les variables pour le récepteur de fichier, l'entrée et le fichier (nous n'initialisons pas ce dernier, car sa valeur dépend de la méthode de transfert - en cliquant sur l'entrée ou en passant dans le récepteur de fichier):

const dropZone = D.querySelector('div')
const input = D.querySelector('input')
let file


Désactivez la gestion par le navigateur des événements de glisser-déposer:



D.addEventListener('dragover', ev => ev.preventDefault())
D.addEventListener('drop', ev => ev.preventDefault())


Afin de comprendre pourquoi nous avons fait cela, essayez de transfĂ©rer une image ou un autre fichier vers le navigateur et voyez ce qui se passe. Et il y a un traitement automatique des fichiers, c'est-Ă -dire ce que nous allons mettre en Ɠuvre nous-mĂȘmes Ă  des fins Ă©ducatives.



Nous gérons le lancement d'un fichier dans le récepteur de fichiers:



dropZone.addEventListener('drop', ev => {
    //    
    ev.preventDefault()

    //   ,  
    log(ev.dataTransfer)

    //   (   )
    /*
    DataTransfer {dropEffect: "none", effectAllowed: "all", items: DataTransferItemList, types: Array(1), files: FileList}
        dropEffect: "none"
        effectAllowed: "all"
    =>  files: FileList
            length: 0
        __proto__: FileList
        items: DataTransferItemList {length: 0}
        types: []
        __proto__: DataTransfer
    */

    //    (File)    "files"  "DataTransfer"
    //  
    file = ev.dataTransfer.files[0]

    // 
    log(file)
    /*
    File {name: "image.png", lastModified: 1593246425244, lastModifiedDate: Sat Jun 27 2020 13:27:05 GMT+0500 (,  ), webkitRelativePath: "", size: 208474, 
}
        lastModified: 1593246425244
        lastModifiedDate: Sat Jun 27 2020 13:27:05 GMT+0500 (,  ) {}
        name: "image.png"
        size: 208474
        type: "image/png"
        webkitRelativePath: ""
        __proto__: File
    */

    //       
    handleFile(file)
})


Nous venons de mettre en place le mécanisme dran'n'drop le plus simple.



Nous traitons le clic sur le récepteur du fichier (nous déléguons le clic à l'entrée):



dropZone.addEventListener('click', () => {
    //    
    input.click()

    //   
    input.addEventListener('change', () => {
        //   ,  
        log(input.files)

        //   (   )
        /*
        FileList {0: File, length: 1}
        =>  0: File
                lastModified: 1593246425244
                lastModifiedDate: Sat Jun 27 2020 13:27:05 GMT+0500 (,  ) {}
                name: "image.png"
                size: 208474
                type: "image/png"
                webkitRelativePath: ""
                __proto__: File
            length: 1
            __proto__: FileList
        */

        //  File
        file = input.files[0]

        // 
        log(file)
        
        //       
        handleFile(file)
    })
})


Commençons par traiter le fichier:



const handleFile = file => {
    //  
}


Nous supprimons le récepteur de fichier et entrons:



dropZone.remove()
input.remove()


Le traitement d'un fichier dépend de son type:



log(file.type)
//   
// image/png


Nous ne travaillerons pas avec les fichiers html, css et js, nous interdisons donc leur traitement:



if (file.type === 'text/html' ||
    file.type === 'text/css' ||
    file.type === 'text/javascript')
return;


Nous ne travaillerons pas non plus avec les fichiers MS (avec le type MIME "application / msword", "application / vnd.ms-excel", etc.), car ils ne peuvent pas ĂȘtre traitĂ©s par des moyens natifs. Toutes les mĂ©thodes de traitement de ces fichiers, proposĂ©es sur StackOverflow et d'autres ressources, se rĂ©sument soit Ă  la conversion vers d'autres formats Ă  l'aide de diverses bibliothĂšques, soit Ă  l'utilisation de visionneuses de Google et Microsoft, qui ne souhaitent pas travailler avec le systĂšme de fichiers et l'hĂŽte local. Dans le mĂȘme temps, le type de fichiers pdf commence Ă©galement par "application", nous allons donc traiter ces fichiers sĂ©parĂ©ment:



if (file.type === 'application/pdf') {
    createIframe(file)
    return;
}


Pour le reste des fichiers, nous obtenons leur type "groupe":



//   ,    
const type = file.type.replace(/\/.+/, '')

// 
log(type)
//   
// image


En utilisant switch..case, nous définissons une fonction de traitement de fichier spécifique:



switch (type) {
    //  
    case 'image':
        createImage(file)
        break;
    //  
    case 'audio':
        createAudio(file)
        break;
    //  
    case 'video':
        createVideo(file)
        break;
    //  
    case 'text':
        createText(file)
        break;
    // ,      ,
    //      
    default:
        B.innerHTML = `<h3>Unknown File Format!</h3>`
        const timer = setTimeout(() => {
            location.reload()
            clearTimeout(timer)
        }, 2000)
        break;
}


Fonction de traitement d'image:



const createImage = image => {
    //   "img"
    const imageEl = D.createElement('img')
    //     
    imageEl.src = URL.createObjectURL(image)
    // 
    log(imageEl)
    //   
    B.append(imageEl)
    //    
    URL.revokeObjectURL(image)
}


Fonction de traitement audio:



const createAudio = audio => {
    //   "audio"
    const audioEl = D.createElement('audio')
    //   
    audioEl.setAttribute('controls', '')
    //     
    audioEl.src = URL.createObjectURL(audio)
    // 
    log(audioEl)
    //   
    B.append(audioEl)
    //  
    audioEl.play()
    //    
    URL.revokeObjectURL(audio)
}


Fonction de traitement vidéo:



const createVideo = video => {
    //   "video"
    const videoEl = D.createElement('video')
    //   
    videoEl.setAttribute('controls', '')
    //  
    videoEl.setAttribute('loop', 'true')
    //     
    videoEl.src = URL.createObjectURL(video)
    // 
    log(videoEl)
    //   
    B.append(videoEl)
    //  
    videoEl.play()
    //    
    URL.revokeObjectURL(video)
}


Fonction de traitement de texte:



const createText = text => {
    //    "FileReader"
    const reader = new FileReader()
    //    
    //    
    //   - utf-8,
    //     
    reader.readAsText(text, 'windows-1251')
    //    
    //     
    reader.onload = () => B.innerHTML = `<p><pre>${reader.result}</pre></p>`
}


DerniĂšre fonction de traitement PDF, mais non la moindre:



const createIframe = pdf => {
    //   "iframe"
    const iframe = D.createElement('iframe')
    //     
    iframe.src = URL.createObjectURL(pdf)
    //         
    iframe.width = innerWidth
    iframe.height = innerHeight
    // 
    log(iframe)
    //   
    B.append(iframe)
    //    
    URL.revokeObjectURL(pdf)
}


RĂ©sultat:







Créer un fichier JSON



Pour le deuxiÚme projet, créez un dossier «Create-JSON» dans le répertoire racine (Work-With-Files-in-JavaScript).



Créez un fichier "index.html" avec le contenu suivant:



<!-- head -->
<!-- materialize css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<!-- material icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

<!-- body -->
<h3>Create JSON</h3>

<!--   -->
<div class="row main">
    <h3>Create JSON</h3>
    <form class="col s12">
        <!--   "-" -->
        <div class="row">
            <div class="input-field col s5">
                <label>key</label>
                <input type="text" value="1" required>
            </div>
            <div class="input-field col s2">
                <p>:</p>
            </div>
            <div class="input-field col s5">
                <label>value</label>
                <input type="text" value="foo" required>
            </div>
        </div>

        <!--   -->
        <div class="row">
            <div class="input-field col s5">
                <label>key</label>
                <input type="text" value="2" required>
            </div>
            <div class="input-field col s2">
                <p>:</p>
            </div>
            <div class="input-field col s5">
                <label>value</label>
                <input type="text" value="bar" required>
            </div>
        </div>
        
        <!--   -->
        <div class="row">
            <div class="input-field col s5">
                <label>key</label>
                <input type="text" value="3" required>
            </div>
            <div class="input-field col s2">
                <p>:</p>
            </div>
            <div class="input-field col s5">
                <label>value</label>
                <input type="text" value="baz" required>
            </div>
        </div>

        <!--  -->
        <div class="row">
            <button class="btn waves-effect waves-light create-json">create json
                <i class="material-icons right">send</i>
            </button>
            <a class="waves-effect waves-light btn get-data"><i class="material-icons right">cloud</i>get data</a>
        </div>
    </form>
</div>


Materialise est utilisé pour le style .







Ajoutez quelques styles personnalisés:



body {
    max-width: 512px;
    margin: 0 auto;
    text-align: center;
}

input {
    text-align: center;
}

.get-data {
    margin-left: 1em;
}


Nous obtenons ce qui suit: Les







fichiers JSON ont le format suivant:



{
    "": "",
    "": "",
    ...
}


Les entrĂ©es impaires de type «texte» sont des clĂ©s, les paires paires sont des valeurs. Nous attribuons des valeurs par dĂ©faut aux entrĂ©es (les valeurs peuvent ĂȘtre n'importe lesquelles). Un bouton avec la classe "create-json" permet de rĂ©cupĂ©rer les valeurs saisies par l'utilisateur et de crĂ©er un fichier. Bouton des classes "get-data" - pour obtenir des donnĂ©es.



Passons au script:



//     "create-json"     
document.querySelector('.create-json').addEventListener('click', ev => {
    //       "submit"  , ..      
    //       
    //    ,    
    ev.preventDefault()

    //   
    const inputs = document.querySelectorAll('input')

    //  ,  
    //     
    //      

    //        "chunk"  "lodash"
    //    (,   ) - 
    //        
    //    (,   ) - 
    //        
    const arr = []
    for (let i = 0; i < inputs.length; ++i) {
        arr.push([inputs[i].value, inputs[++i].value])
    }

    //  ,    
    console.log(arr)
    /* 
        [
            ["1", "foo"]
            ["2", "bar"]
            ["3", "baz"]
        ]
    */

    //     
    const data = Object.fromEntries(arr)

    // 
    console.log(data)
    /* 
        {
            1: "foo"
            2: "bar"
            3: "baz"
        }
    */
    
    //  
    const file = new Blob(
        //  
        [JSON.stringify(data)], {
            type: 'application/json'
        }
    )
    
    // 
    console.log(file)
    /* 
        {
            "1": "foo",
            "2": "bar",
            "3": "baz"
        }
    */
    // ,   

    //   "a"
    const link = document.createElement('a')
    //   "href"  "a"   
    link.setAttribute('href', URL.createObjectURL(file))
    //  "download"   ,    
    //    -   
    link.setAttribute('download', 'data.json')
    //   
    link.textContent = 'DOWNLOAD DATA'
    //       "main"
    document.querySelector('.main').append(link)
    //    
    URL.revokeObjectURL(file)

    // { once: true }      
    //      
}, { once: true })


En cliquant sur le bouton "CREER JSON", le fichier "data.json" est généré, le lien "DOWNLOAD DATA" apparaßt pour télécharger ce fichier.



Que pouvons-nous faire avec ce fichier? Téléchargez-le et placez-le dans le dossier "Create-JSON".



On a:



//   (    )   "get-data"    
document.querySelector('.get-data').addEventListener('click', () => {
    //   IIFE  async..await          
    (async () => {
        const response = await fetch('data.json')

        //  () 
        const data = await response.json()

        console.table(data)
    })()
})


RĂ©sultat:







Créer un générateur de questions et un testeur



Générateur de questions


Pour le troisiÚme projet, créons un dossier «Test-Maker» dans le répertoire racine.



Créez un fichier "createTest.html" avec le contenu suivant:



<!-- head -->
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
    integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- body -->
<!--   -->
<div class="container">
    <h3>Create Test</h3>
    <form id="questions-box">
        <!--    -->
        <div class="question-box">
            <br><hr>
            <h4 class="title"></h4>
            <!--  -->
            <div class="row">
                <input type="text" class="form-control col-11 question-text" value="first question" >
                <!--    -->
                <button class="btn btn-danger col remove-question-btn">X</button>
            </div>
            <hr>

            <h4>Answers:</h4>
            <!--   -->
            <div class="row answers-box">
                <!--   -->
                <div class="input-group">
                    <div class="input-group-prepend">
                        <div class="input-group-text">
                            <input type="radio" checked name="answer">
                        </div>
                    </div>

                    <input class="form-control answer-text" type="text" value="foo" >

                    <!--     -->
                    <div class="input-group-append">
                        <button class="btn btn-outline-danger remove-answer-btn">X</button>
                    </div>
                </div>
                <!--   -->
                <div class="input-group">
                    <div class="input-group-prepend">
                        <div class="input-group-text">
                            <input type="radio" name="answer">
                        </div>
                    </div>

                    <input class="form-control answer-text" type="text" value="bar" >

                    <div class="input-group-append">
                        <button class="btn btn-outline-danger remove-answer-btn">X</button>
                    </div>
                </div>
                <!--   -->
                <div class="input-group">
                    <div class="input-group-prepend">
                        <div class="input-group-text">
                            <input type="radio" name="answer">
                        </div>
                    </div>

                    <input class="form-control answer-text" type="text" value="baz" >

                    <div class="input-group-append">
                        <button class="btn btn-outline-danger remove-answer-btn">X</button>
                    </div>
                </div>
            </div>
            <br>

            <!--      -->
            <button class="btn btn-primary add-answer-btn">Add answer</button>
            <hr>

            <h4>Explanation:</h4>
            <!--  -->
            <div class="row explanation-box">
                <input type="text" value="first explanation" class="form-control explanation-text" >
            </div>
        </div>
    </form>

    <br>

    <!--        -->
    <button class="btn btn-primary" id="add-question-btn">Add question</button>
    <button class="btn btn-primary" id="create-test-btn">Create test</button>
</div>


Cette fois, Bootstrap est utilisé pour le style . Nous n'utilisons pas les attributs "requis" puisque nous allons valider le formulaire dans JS (avec requis, le comportement d'un formulaire composé de plusieurs champs obligatoires devient ennuyeux).







Ajoutez quelques styles personnalisés:



body {
        max-width: 512px;
        margin: 0 auto;
        text-align: center;
    }

input[type="radio"] {
    cursor: pointer;
}


Nous







obtenons ce qui suit: Nous avons un modÚle de question. Je propose de le déplacer dans un fichier séparé pour une utilisation en tant que composant en utilisant l'importation dynamique. Créez un fichier "Question.js" avec le contenu suivant:



export default (name = Date.now()) => `
<div class="question-box">
    <br><hr>
    <h4 class="title"></h4>
    <div class="row">
        <input type="text" class="form-control col-11 question-text">
        <button class="btn btn-danger col remove-question-btn">X</button>
    </div>
    <hr>
    <h4>Answers:</h4>
    <div class="row answers-box">
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="radio" checked name="${name}">
                </div>
            </div>

            <input class="form-control answer-text" type="text" >

            <div class="input-group-append">
                <button class="btn btn-outline-danger remove-answer-btn">X</button>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="radio" name="${name}">
                </div>
            </div>

            <input class="form-control answer-text" type="text" >

            <div class="input-group-append">
                <button class="btn btn-outline-danger remove-answer-btn">X</button>
            </div>
        </div>
        <div class="input-group">
            <div class="input-group-prepend">
                <div class="input-group-text">
                    <input type="radio" name="${name}">
                </div>
            </div>

            <input class="form-control answer-text" type="text" >

            <div class="input-group-append">
                <button class="btn btn-outline-danger remove-answer-btn">X</button>
            </div>
        </div>
    </div>
    <br>
    <button class="btn btn-primary add-answer-btn">Add answer</button>
    <hr>
    <h4>Explanation:</h4>
    <div class="row explanation-box">
        <input type="text" class="form-control explanation-text">
    </div>
</div>
`


Ici, tout est identique Ă  celui de createTest.html, sauf que nous avons supprimĂ© les valeurs par dĂ©faut des entrĂ©es et passĂ© l'argument "name" comme valeur de l'attribut du mĂȘme nom (cet attribut doit ĂȘtre unique pour chaque question - cela le rend changer les options de rĂ©ponse, choisissez l'une parmi plusieurs). La valeur par dĂ©faut de name est le temps en millisecondes depuis le 1er janvier 1970, une alternative simple aux gĂ©nĂ©rateurs de valeurs alĂ©atoires Nanoid utilisĂ©s pour obtenir un identifiant unique (l'utilisateur n'aura probablement pas le temps de crĂ©er deux questions en 1 ms).



Passons au script principal.



Je vais créer des fonctions d'assistance (d'usine), mais ce n'est pas nécessaire.



Fonctions secondaires:



//       
const findOne = (element, selector) => element.querySelector(selector)
//       
const findAll = (element, selector) => element.querySelectorAll(selector)
//     
const addHandler = (element, event, callback) => element.addEventListener(event, callback)

//    
//    Bootstrap    ,
//    DOM        
//      -    (),
//     1
const findParent = (element, depth = 1) => {
    //       ,
    // ,     
    let parentEl = element.parentElement

    // ,      ..   
    while (depth > 1) {
        // 
        parentEl = findParent(parentEl)
        //   
        depth--
    }

    //   
    return parentEl
}


Dans notre cas, à la recherche de l'élément parent, nous atteindrons le troisiÚme niveau d'imbrication. Puisque nous connaissons le nombre exact de ces niveaux, nous aurions pu utiliser if..else if ou switch..case, mais l'option récursivité est plus polyvalente.



Encore une fois: il n'est pas nécessaire d'introduire des fonctions d'usine, vous pouvez facilement vous en tirer avec la fonctionnalité standard.



Recherchez le conteneur principal et le conteneur pour les questions, et désactivez également la soumission du formulaire:



const C = findOne(document.body, '.container')
// const C = document.body.querySelector('.container')
const Q = findOne(C, '#questions-box')

addHandler(Q, 'submit', ev => ev.preventDefault())
// Q.addEventListener('submit', ev => ev.preventDefault())


Fonction d'initialisation du bouton pour supprimer la question:



//      
const initRemoveQuestionBtn = q => {
    const removeQuestionBtn = findOne(q, '.remove-question-btn')

    addHandler(removeQuestionBtn, 'click', ev => {
        //    
        /*
        =>  <div class="question-box">
                <br><hr>
                <h4 class="title"></h4>
            =>  <div class="row">
                    <input type="text" class="form-control col-11 question-text" value="first question" >
                =>  <button class="btn btn-danger col remove-question-btn">X</button>
                </div>

                ...
        */
        findParent(ev.target, 2).remove()
        // ev.target.parentElement.parentElement.remove()

        //       
        initTitles()
    }, {
        //    
        once: true
    })
}


Fonction d'initialisation du bouton pour supprimer l'option de réponse:



const initRemoveAnswerBtns = q => {
    const removeAnswerBtns = findAll(q, '.remove-answer-btn')
    // const removeAnswerBtns = q.querySelectorAll('.remove-answer-btn')

    removeAnswerBtns.forEach(btn => addHandler(btn, 'click', ev => {
        /*
        =>  <div class="input-group">
              ...

          =>  <div class="input-group-append">
              =>  <button class="btn btn-outline-danger remove-answer-btn">X</button>
              </div>
            </div>
        */
        findParent(ev.target, 2).remove()
    }, {
        once: true
    }))
}


Fonction d'initialisation du bouton pour ajouter une option de réponse:



const initAddAnswerBtns = q => {
    const addAnswerBtns = findAll(q, '.add-answer-btn')

    addAnswerBtns.forEach(btn => addHandler(btn, 'click', ev => {
        //    
        const answers = findOne(findParent(ev.target), '.answers-box')
        // const answers = ev.target.parentElement.querySelector('.answers-box')

        //  "name"      
        let name
        answers.children.length > 0
            ? name = findOne(answers, 'input[type="radio"]').name
            : name = Date.now()

        //   
        const template = `
                <div class="input-group">
                    <div class="input-group-prepend">
                        <div class="input-group-text">
                            <input type="radio" name="${name}">
                        </div>
                    </div>

                    <input class="form-control answer-text" type="text" value="">

                    <div class="input-group-append">
                        <button class="btn btn-outline-danger remove-answer-btn">X</button>
                    </div>
                </div>
                `
        //       
        answers.insertAdjacentHTML('beforeend', template)
        
        //      
        initRemoveAnswerBtns(q)
    }))
}


Nous combinons les fonctions d'initialisation des boutons en un seul:



const initBtns = q => {
    initRemoveQuestionBtn(q)
    initRemoveAnswerBtns(q)
    initAddAnswerBtns(q)
}


Fonction d'initialisation des en-tĂȘtes de question:



const initTitles = () => {
    //          
    const questions = Array.from(findAll(Q, '.question-box'))

    //  
    questions.map(q => {
        const title = findOne(q, '.title')
        //   -    + 1
        title.textContent = `Question ${questions.indexOf(q) + 1}`
    })
}


Initialisons les boutons et le titre de la question:



initBtns(findOne(Q, '.question-box'))

initTitles()


Ajouter une fonction de question:



//  
const addQuestionBtn = findOne(C, '#add-question-btn')

addHandler(addQuestionBtn, 'click', ev => {
    //   IIFE  async..await     
    //      
    //   
    //        
    (async () => {
        const data = await import('./Question.js')
        const template = await data.default()
        await Q.insertAdjacentHTML('beforeend', template)

        const question = findOne(Q, '.question-box:last-child')
        initBtns(question)
        initTitles()
    })()
})


Fonction de création de test:



//       
addHandler(findOne(C, '#create-test-btn'), 'click', () => createTest())

const createTest = () => {
    //   
    const obj = {}

    //   
    const questions = findAll(Q, '.question-box')

    //    
    //     
    const isEmpty = (...args) => {
        //    
        args.map(arg => {
            //       
            //        
            arg = arg.replace(/\s+/g, '').trim()
            //      
            if (arg === '') {
                //    
                alert('Some field is empty!')
                //   
                throw new Error()
            }
        })
    }

    //   
    questions.forEach(q => {
        //  
        const questionText = findOne(q, '.question-text').value

        //     
        //     
        const answersText = []
        findAll(q, '.answer-text').forEach(text => answersText.push(text.value))
        
        //    -          "checked"    "answer-text"
        /*
        =>  <div class="input-group">
              <div class="input-group-prepend">
                <div class="input-group-text">
              =>  <input type="radio" checked name="answer">
                </div>
              </div>

          => <input class="form-control answer-text" type="text" value="foo" >

          ...
        */
        const rightAnswerText = findOne(findParent(findOne(q, 'input:checked'), 3), '.answer-text').value

        //  
        const explanationText = findOne(q, '.explanation-text').value

        //  
        isEmpty(questionText, ...answersText, explanationText)
        
        //       " "
        obj[questions.indexOf(q)] = {
            question: questionText,
            answers: answersText,
            rightAnswer: rightAnswerText,
            explanation: explanationText
        }
    })

    // 
    console.table(obj)

    //  
    const data = new Blob(
        [JSON.stringify(obj)], {
            type: 'application/json'
        }
    )
    
    //    
    //  
    if (findOne(C, 'a') !== null) {
        findOne(C, 'a').remove()
    }
    
    //   
    const link = document.createElement('a')
    link.setAttribute('href', URL.createObjectURL(data))
    link.setAttribute('download', 'data.json')
    link.className = 'btn btn-success'
    link.textContent = 'Download data'
    C.append(link)
    URL.revokeObjectURL(data)
}


RĂ©sultat:







Utilisation des données d'un fichier


À l'aide du gĂ©nĂ©rateur de questions, crĂ©ez un fichier comme celui-ci:



{
    "0": {
        "question": "first question",
        "answers": ["foo", "bar", "baz"],
        "rightAnswer": "foo",
        "explanation": "first explanation"
    },
    "1": {
        "question": "second question",
        "answers": ["foo", "bar", "baz"],
        "rightAnswer": "bar",
        "explanation": "second explanation"
    },
    "2": {
        "question": "third question",
        "answers": ["foo", "bar", "baz"],
        "rightAnswer": "baz",
        "explanation": "third explanation"
    }
}


Placez ce fichier (data.json) dans le dossier "Test-Maker".



Créez un fichier "useData.html" avec le contenu suivant:



<!-- head -->
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
        integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">

<!-- body -->
<h1>Use data</h1>


Ajoutez quelques styles personnalisés:



body {
    max-width: 512px;
    margin: 0 auto;
    text-align: center;
}

section *:not(h3) {
    text-align: left;
}

input,
button {
    margin: .4em;
}

label,
input {
    cursor: pointer;
}

.right-answer,
.explanation {
    display: none;
}


Scénario:



//   
const getData = async url => {
    const response = await fetch(url)
    const data = await response.json()
    return data
}

//  
getData('data.json')
    .then(data => {
        // 
        console.table(data)
        
        //     
        createTest(data)
    })

//   name
let name = Date.now()
//   
const createTest = data => {
    // data -    
    //   
    for (const item in data) {
        // 
        console.log(data[item])

        //  ,
        //   ,  ,    
        const {
            question,
            answers,
            rightAnswer,
            explanation
        } = data[item]

        //   name    
        name++

        //  
        const questionTemplate = `
            <hr>
            <section>
                <h3>Question ${item}: ${question}</h3>
                <form>
                    <legend>Answers</legend>
                    ${answers.reduce((html, ans) => html += `<label><input type="radio" name="${name}">${ans}</label><br>`, '')}
                </form>
                <p class="right-answer">Right answer: ${rightAnswer}</p>
                <p class="explanation">Explanation: ${explanation}</p>
            </section>
        `
        
        //     
        document.body.insertAdjacentHTML('beforeend', questionTemplate)
    })

    //  
    const forms = document.querySelectorAll('form')

    //       
    forms.forEach(form => {
        const input = form.querySelector('input')
        input.click()
    })

    //     
    //      
    const btn = document.createElement('button')
    btn.className = 'btn btn-primary'
    btn.textContent = 'Check answers'
    document.body.append(btn)

    //    
    btn.addEventListener('click', () => {
        //    
        const answers = []

        //   
        forms.forEach(form => {
            //    () 
            const chosenAnswer = form.querySelector('input:checked').parentElement.textContent
            //    
            const rightAnswer = form.nextElementSibling.textContent.replace('Right answer: ', '')
            //        
            answers.push([chosenAnswer, rightAnswer])
        })

        console.log(answers)
        //  
        //  ,      
        /*
        Array(3)
            0: (2) ["foo", "foo"]
            1: (2) ["bar", "bar"]
            2: (2) ["foo", "baz"]
        */

        //   
        checkAnswers(answers)
    })

    //   () 
    const checkAnswers = answers => {
        //       
        let rightAnswers = 0
        let wrongAnswers = 0

        //   ,
        //    -  () ,
        //    -  
        for (const answer of answers) {
            //      
            if (answer[0] === answer[1]) {
                //    
                rightAnswers++
            // 
            } else {
                //    
                wrongAnswers++

                //     
                const wrongSection = forms[answers.indexOf(answer)].parentElement

                //     
                wrongSection.querySelector('.right-answer').style.display = 'block'
                wrongSection.querySelector('.explanation').style.display = 'block'
            }
        }

        //    
        const percent = parseInt(rightAnswers / answers.length * 100)

        // -
        let result = ''
        
        //      
        //  result  
        if (percent >= 80) {
            result = 'Great job, super genius!'
        } else if (percent > 50) {
            result = 'Not bad, but you can do it better!'
        } else {
            result = 'Very bad, try again!'
        }

        //   
        const resultTemplate = `
            <h3>Your result</h3>
            <p>Right answers: ${rightAnswers}</p>
            <p>Wrong answers: ${wrongAnswers}</p>
            <p>Percentage of correct answers: ${percent}</p>
            <p>${result}</p>
        `
        
        //     
        document.body.insertAdjacentHTML('beforeend', resultTemplate)
    }
}


RĂ©sultat (au cas oĂč la rĂ©ponse Ă  la troisiĂšme question est erronĂ©e):











Prime. Écrire des donnĂ©es sur CloudFlare



Allez sur cloudflare.com , inscrivez-vous, cliquez sur Travailleurs à droite, puis cliquez sur le bouton "Créer un travailleur".











Changez le nom du travailleur en "données" (ceci est facultatif). Dans le champ "{} Script", insérez le code suivant et cliquez sur le bouton "Enregistrer et déployer":



//   
addEventListener('fetch', event => {
    event.respondWith(
        new Response(
            //  
            `{
                "0": {
                    "question": "first question",
                    "answers": ["foo", "bar", "baz"],
                    "rightAnswer": "foo",
                    "explanation": "first explanation"
                },
                "1": {
                    "question": "second question",
                    "answers": ["foo", "bar", "baz"],
                    "rightAnswer": "bar",
                    "explanation": "second explanation"
                },
                "2": {
                    "question": "third question",
                    "answers": ["foo", "bar", "baz"],
                    "rightAnswer": "baz",
                    "explanation": "third explanation"
                }
            }`,
            {
                status: 200,
                //     CORS
                headers: new Headers({'Access-Control-Allow-Origin': '*'})
            })
        )
    })






Nous pouvons désormais recevoir des données de CloudFlare. Pour ce faire, spécifiez simplement l'URL du travailleur au lieu de 'data.json' dans la fonction "getData". Dans mon cas, cela ressemble à ceci: getData ('https://data.aio350.workers.dev/') .then (...).



Un long article s'est avéré. J'espÚre que vous y avez trouvé quelque chose d'utile.



Merci de votre attention.



All Articles