العمل مع الملفات في JavaScript

يوم جيد ، أيها الأصدقاء!



الرأي القائل بأن JavaScript لا يعرف كيفية التفاعل مع نظام الملفات ليس صحيحًا تمامًا. بدلاً من ذلك ، يتعلق الأمر بحقيقة أن هذا التفاعل محدود بشكل كبير مقارنة بلغات البرمجة من جانب الخادم مثل Node.js أو PHP. ومع ذلك ، فإن JavaScript قادر على تلقي (استقبال) وإنشاء بعض أنواع الملفات ومعالجتها محليًا بنجاح.



في هذا المقال سننشئ ثلاثة مشاريع صغيرة:



  • نقوم بتنفيذ استلام ومعالجة الصور والصوت والفيديو والنصوص بتنسيق txt و pdf
  • لنقم بإنشاء مولد ملفات JSON
  • دعنا نكتب برنامجين: أحدهما سيشكل أسئلة (بتنسيق JSON) ، والآخر سيستخدمها لإنشاء اختبار


إذا كنت مهتمًا ، من فضلك اتبعني.



كود المشروع على جيثب .



نستقبل الملفات ونعالجها



أولاً ، لنقم بإنشاء دليل حيث سيتم تخزين مشاريعنا. دعنا نسميها "Work-With-Files-in-JavaScript" أو ما تريد.



في هذا الدليل ، قم بإنشاء مجلد للمشروع الأول. دعنا نسميها "قارئ الملفات".



قم بإنشاء ملف "index.html" فيه بالمحتوى التالي:



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


هنا لدينا مستقبل ملف الحاوية ومدخل من نوع "ملف" (للحصول على ملف ؛ سنعمل مع ملفات فردية ؛ للحصول على ملفات متعددة ، يجب إضافة المدخلات بالسمة "متعددة") ، والتي ستكون مخفية تحت الحاوية.



يمكن تضمين الأنماط في ملف منفصل أو في علامة "النمط" داخل الرأس:



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;
}


يمكنك جعل التصميم يرضيك.



لا تنس تضمين النص البرمجي إما في الرأس مع سمة "تأجيل" (نحتاج إلى الانتظار حتى يتم رسم DOM (يتم تقديمه) ؛ يمكنك بالطبع القيام بذلك في النص البرمجي عن طريق معالجة حدث "التحميل" أو "DOMContentLoaded" لكائن "النافذة" ، ولكن التأجيل أقصر بكثير) ، أو قبل علامة الإغلاق "body" (فلا حاجة إلى سمة أو معالج). أنا شخصيا أفضل الخيار الأول.



لنفتح index.html في المتصفح:







قبل الشروع في كتابة البرنامج النصي ، نحتاج إلى تجهيز الملفات للتطبيق: نحتاج إلى صورة وصوت وفيديو ونص بصيغة txt و pdf وأي تنسيق آخر ، على سبيل المثال ، doc. يمكنك استخدام مجموعتي أو بناء مجموعتك الخاصة.



غالبًا ما يتعين علينا الوصول إلى كائنات "document" و "document.body" ، بالإضافة إلى إخراج النتائج إلى وحدة التحكم عدة مرات ، لذا أقترح التفاف الكود الخاص بنا في IIFE (هذا ليس ضروريًا):



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

    //  

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


بادئ ذي بدء ، نعلن عن المتغيرات الخاصة بمستقبل الملف والمدخلات والملف (لا نقوم بتهيئة الأخير ، نظرًا لأن قيمته تعتمد على طريقة النقل - من خلال النقر على الإدخال أو الإسقاط في مستقبل الملف):

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


تعطيل تعامل المتصفح مع أحداث "السحب والإفلات":



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


لفهم سبب قيامنا بذلك ، حاول نقل صورة أو ملف آخر إلى المتصفح وشاهد ما سيحدث. وهناك معالجة تلقائية للملفات ، أي ما سنقوم بتنفيذه بأنفسنا للأغراض التعليمية.



نتعامل مع رمي ملف في مستقبل الملفات:



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)
})


لقد قمنا للتو بتنفيذ أبسط آلية dran'n'drop.



نقوم بمعالجة النقر على ملف المتلقي (نقوم بتفويض النقرة للإدخال):



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)
    })
})


لنبدأ في معالجة الملف:



const handleFile = file => {
    //  
}


نحذف ملف المتلقي والمدخلات:



dropZone.remove()
input.remove()


تعتمد طريقة معالجة الملف على نوعه:



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


لن نعمل مع ملفات html و css و js ، لذلك نحظر معالجتها:



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


لن نعمل أيضًا مع ملفات MS (مع نوع MIME "application / msword" ، "application / vnd.ms-excel" ، إلخ) ، نظرًا لأنه لا يمكن معالجتها بالوسائل الأصلية. تنخفض جميع طرق معالجة هذه الملفات ، المقدمة على StackOverflow والموارد الأخرى ، إما إلى التحويل إلى تنسيقات أخرى باستخدام مكتبات مختلفة ، أو استخدام عارضين من Google و Microsoft ، الذين لا يريدون العمل مع نظام الملفات والمضيف المحلي. في الوقت نفسه ، يبدأ نوع ملفات pdf أيضًا بـ "application" ، لذلك سنقوم بمعالجة هذه الملفات بشكل منفصل:



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


بالنسبة لبقية الملفات ، نحصل على نوع "المجموعة" الخاص بهم:



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

// 
log(type)
//   
// image


باستخدام switch..case ، نحدد وظيفة معالجة ملف معينة:



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;
}


وظيفة معالجة الصور:



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


وظيفة معالجة الصوت:



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)
}


وظيفة معالجة الفيديو:



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)
}


وظيفة معالجة النص:



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>`
}


أخيرًا وليس آخرًا ، وظيفة معالجة pdf:



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)
}


نتيجة:







أنشئ ملف JSON



بالنسبة للمشروع الثاني ، أنشئ مجلد "Create-JSON" في الدليل الجذر (Work-With-Files-in-JavaScript).



قم بإنشاء ملف "index.html" بالمحتوى التالي:



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


وتتحقق المستخدمة في التصميم .







أضف نوعين من الأنماط المخصصة:



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

input {
    text-align: center;
}

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


نحصل على ما يلي:







ملفات JSON لها التنسيق التالي:



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


المدخلات الفردية من نوع "نص" هي مفاتيح ، حتى منها عبارة عن قيم. نقوم بتعيين القيم الافتراضية للمدخلات (يمكن أن تكون القيم أيًا). يتم استخدام زر به فئة "create-json" للحصول على القيم التي أدخلها المستخدم وإنشاء ملف. زر من فئات "الحصول على البيانات" - للحصول على البيانات.



دعنا ننتقل إلى النص:



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


بالنقر فوق الزر "CREATE JSON" ، يتم إنشاء ملف "data.json" ، يظهر رابط "DOWNLOAD DATA" لتنزيل هذا الملف.



ماذا يمكننا أن نفعل بهذا الملف؟ قم بتنزيله ووضعه في مجلد "Create-JSON".



نحن نحصل:



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


نتيجة:







إنشاء منشئ الأسئلة والمختبِر



منشئ الأسئلة


بالنسبة للمشروع الثالث ، لنقم بإنشاء مجلد "Test-Maker" في الدليل الجذر.



قم بإنشاء ملف "createTest.html" بالمحتوى التالي:



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


هذه المرة ، يتم استخدام Bootstrap للتصميم . نحن لا نستخدم السمات "المطلوبة" لأننا سنقوم بالتحقق من صحة النموذج في JS (مع المطلوب ، يصبح سلوك نموذج يتكون من عدة حقول مطلوبة أمرًا مزعجًا).







أضف نوعين من الأنماط المخصصة:



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

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


نحصل







على ما يلي: لدينا نموذج أسئلة. أقترح نقله إلى ملف منفصل لاستخدامه كمكون باستخدام الاستيراد الديناميكي. قم بإنشاء ملف "Question.js" بالمحتوى التالي:



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


هنا لدينا كل شيء كما هو الحال في createTest.html ، باستثناء أننا أزلنا القيم الافتراضية للمدخلات وتمرير وسيطة "الاسم" كقيمة للسمة التي تحمل الاسم نفسه (يجب أن تكون هذه السمة فريدة لكل سؤال - وهذا يجعل ذلك ممكنًا تبديل خيارات الإجابة ، اختر واحدًا من عدة خيارات). القيمة الافتراضية للاسم هي الوقت بالمللي ثانية منذ 1 يناير 1970 ، وهي بديل بسيط لمولدات القيمة العشوائية النانوية المستخدمة للحصول على معرف فريد (من غير المحتمل أن يكون لدى المستخدم الوقت لإنشاء سؤالين في 1 مللي ثانية).



دعنا ننتقل إلى النص الرئيسي.



سأقوم بإنشاء بعض الوظائف المساعدة (المصنع) ، لكن هذا ليس ضروريًا.



الوظائف الثانوية:



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


في حالتنا ، عند البحث عن العنصر الأصل ، سنصل إلى مستوى التداخل الثالث. نظرًا لأننا نعرف العدد الدقيق لهذه المستويات ، فقد استخدمنا if .. else if or switch..case ، لكن خيار العودية أكثر تنوعًا.



مرة أخرى: ليس من الضروري إدخال وظائف المصنع ، يمكنك بسهولة الحصول على الوظائف القياسية.



ابحث عن الحاوية والحاوية الرئيسية للأسئلة ، وقم أيضًا بتعطيل إرسال النموذج:



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())


وظيفة تهيئة الزر لحذف السؤال:



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


وظيفة تهيئة الزر لحذف خيار الإجابة:



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
    }))
}


وظيفة تهيئة الزر لإضافة خيار الإجابة:



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)
    }))
}


نقوم بدمج وظائف أزرار التهيئة في واحد:



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


وظيفة لتهيئة رؤوس الأسئلة:



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}`
    })
}


لنبدأ في تهيئة الأزرار وعنوان السؤال:



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

initTitles()


إضافة وظيفة السؤال:



//  
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()
    })()
})


وظيفة إنشاء الاختبار:



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


نتيجة:







استخدام بيانات من ملف


باستخدام منشئ الأسئلة ، أنشئ ملفًا مثل هذا:



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


ضع هذا الملف (data.json) في مجلد "Test-Maker".



قم بإنشاء ملف "useData.html" بالمحتوى التالي:



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


أضف نوعين من الأنماط المخصصة:



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;
}


النصي:



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


النتيجة (في حالة خطأ إجابة السؤال الثالث):











علاوة. كتابة البيانات إلى CloudFlare



انتقل إلى cloudflare.com ، وقم بالتسجيل ، وانقر فوق العمال على اليمين ، ثم انقر فوق الزر "إنشاء عامل".











قم بتغيير اسم العامل إلى "بيانات" (هذا اختياري). في الحقل "{} Script" ، أدخل الكود التالي وانقر على الزر "Save and Deploy":



//   
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': '*'})
            })
        )
    })






يمكننا الآن تلقي البيانات من CloudFlare. للقيام بذلك ، تحتاج فقط إلى تحديد عنوان URL الخاص بالعامل بدلاً من "data.json" في وظيفة "getData". في حالتي ، يبدو الأمر كما يلي: getData ('https://data.aio350.workers.dev/') ، ثم (...).



تحولت مقالة طويلة. أتمنى أن تجد شيئًا مفيدًا فيه.



شكرآ لك على أهتمامك.



All Articles