[JavaScript, Программирование, Разработка веб-сайтов] Работа с файлами в JavaScript
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Доброго времени суток, друзья!
Мнение о том, что JavaScript не умеет взаимодействовать с файловой системой, является не совсем верным. Скорее, речь идет о том, что это взаимодействие существенно ограничено по сравнению с серверными языками программирования, такими как Node.js или PHP. Тем не менее, JavaScript умеет как получать (принимать), так и создавать некоторые типы файлов и успешно обрабатывать их нативными средствами.
В этой статье мы создадим три небольших проекта:
- Реализуем получение и обработку изображений, аудио, видео и текста в формате txt и pdf
- Создадим генератор JSON-файлов
- Напишем две программы: одна будет формировать вопросы (в формате JSON), а другая использовать их для создания теста
Если Вам это интересно, прошу следовать за мной.
Код проекта на GitHub.
Получаем и обрабатываем файлы
Для начала создадим директорию, в которой будут храниться наши проекты. Назовем ее «Work-With-Files-in-JavaScript» или как Вам будет угодно.
В этой директории создадим папку для первого проекта. Назовем ее «File-Reader».
Создаем в ней файл «index.html» следующего содержания:
<div>+</div>
<input type="file">
Здесь мы имеем контейнер-файлоприемник и инпут с типом «file» (для получения файла; мы будем работать с одиночными файлами; для получения нескольких файлов инпуту следует добавить атрибут «multiple»), который будет спрятан под контейнером.
Стили можно подключить отдельным файлом или в теге «style» внутри head:
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;
}
Можете сделать дизайн по своему вкусу.
Не забываем подключить скрипт либо в head с атрибутом «defer» (нам нужно дождаться отрисовки (рендеринга) DOM; можно, конечно, сделать это в скрипте через обработку события «load» или «DOMContentLoaded» объекта «window», но defer намного короче), либо перед закрывающим тегом «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)
Первым делом объявляем переменные для файлоприемника, инпута и файла (последний не инициализируем, поскольку его значение зависит от способа передачи — через клик по инпуту или бросание (drop) в файлоприемник):
const dropZone = D.querySelector('div')
const input = D.querySelector('input')
let file
Отключаем обработку событий «dragover» и «drop» браузером:
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 и других ресурсах, сводятся либо к конвертации в другие форматы с помощью различных библиотек, либо к использованию viewer'ов от Google и Microsoft, которые не хотят работать с файловой системой и localhost. Вместе с тем, тип 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>`
}
Last, but not least, функция обработки 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>
Для стилизации используется Materialize.
Добавляем парочку собственных стилей:
body {
max-width: 512px;
margin: 0 auto;
text-align: center;
}
input {
text-align: center;
}
.get-data {
margin-left: 1em;
}
Получаем следующее:
JSON-файлы имеют следующий формат:
{
"ключ": "значение",
"ключ": "значение",
...
}
Нечетные инпуты с типом «text» — ключи, четные — значения. Присваиваем инпутам значения по умолчанию (значения могут быть любыми). Кнопка с классом «create-json» служит для получения значений, введенных пользователем, и создания файла. Кнопка с классов «get-data» — для получения данных.
Переходим к скрипту:
// находим кнопку с классом "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. Мы не используем атрибуты «required», поскольку будем валидировать форму в JS (с required поведение формы, состоящей из нескольких обязательных полей, становится раздражающим).
Добавляем парочку собственных стилей:
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, за исключением того, что мы убрали значения по умолчанию для инпутов и передаем аргумент «name» в качестве значения одноименного атрибута (данный атрибут должен быть уникальным для каждого вопроса — это дает возможность переключать варианты ответов, выбирать один из нескольких). Значением name по умолчанию является время в миллисекундах, прошедшее с 1 января 1970 года, — простая альтернатива генераторам случайных значений типа Nanoid, используемых для получения уникального идентификатора (вряд ли пользователь успеет создать два вопроса за 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 или 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, регистрируемся, нажимаем на Workers справа, затем на кнопку «Create a Worker».
Меняем название воркера на «data» (это не обязательно). В поле "{} 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/').then(...).
Длинная статья получилась. Надеюсь, Вы нашли в ней для себя что-то полезное.
Благодарю за внимание.
===========
Источник:
habr.com
===========
Похожие новости:
- [Big Data, Python, Машинное обучение, Программирование] Почему стоит начать использовать FastAPI прямо сейчас (перевод)
- [Программирование, Системное программирование, Читальный зал] Программное обеспечение дешевле в мелкой таре (перевод)
- [Программирование, История IT] История IT. Когда компьютеры были большими…
- [JavaScript, ReactJS, Разработка веб-сайтов] Debouncing с помощью React Hooks: хук для функций
- [Программирование, Проектирование и рефакторинг, Совершенный код] Поговорим о код-ревью
- [Веб-аналитика, Веб-дизайн, Медийная реклама, Разработка веб-сайтов] Игра не по правилам: опыт участия в известном российском конкурсе проектов
- [UML Design, Анализ и проектирование систем, Программирование, Разработка веб-сайтов] UML для самых маленьких: диаграмма классов
- [OpenStreetMap, Геоинформационные сервисы, Интервью, Работа с 3D-графикой, Разработка игр] Роман Шувалов: «Мне пришла в голову идея — сделать трехмерный рендер карты OpenStreetMap»
- [Разработка веб-сайтов, Разработка мобильных приложений] Lamptest.ru: 5 лет, 3500 ламп, новые возможности
- [Программирование] Как и почему компьютеры кидают шулерские кости (перевод)
Теги для поиска: #_javascript, #_programmirovanie (Программирование), #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_programmirovanie (программирование), #_razrabotka (разработка), #_file_system, #_file_api, #_file_reader, #_file_upload, #_chtenie_fajlov (чтение файлов), #_zagruzka_fajlov (загрузка файлов), #_obrabotka_fajlov (обработка файлов), #_javascript, #_programmirovanie (
Программирование
), #_razrabotka_vebsajtov (
Разработка веб-сайтов
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:43
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Доброго времени суток, друзья! Мнение о том, что JavaScript не умеет взаимодействовать с файловой системой, является не совсем верным. Скорее, речь идет о том, что это взаимодействие существенно ограничено по сравнению с серверными языками программирования, такими как Node.js или PHP. Тем не менее, JavaScript умеет как получать (принимать), так и создавать некоторые типы файлов и успешно обрабатывать их нативными средствами. В этой статье мы создадим три небольших проекта:
Если Вам это интересно, прошу следовать за мной. Код проекта на GitHub. Получаем и обрабатываем файлы Для начала создадим директорию, в которой будут храниться наши проекты. Назовем ее «Work-With-Files-in-JavaScript» или как Вам будет угодно. В этой директории создадим папку для первого проекта. Назовем ее «File-Reader». Создаем в ней файл «index.html» следующего содержания: <div>+</div>
<input type="file"> Здесь мы имеем контейнер-файлоприемник и инпут с типом «file» (для получения файла; мы будем работать с одиночными файлами; для получения нескольких файлов инпуту следует добавить атрибут «multiple»), который будет спрятан под контейнером. Стили можно подключить отдельным файлом или в теге «style» внутри head: 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; } Можете сделать дизайн по своему вкусу. Не забываем подключить скрипт либо в head с атрибутом «defer» (нам нужно дождаться отрисовки (рендеринга) DOM; можно, конечно, сделать это в скрипте через обработку события «load» или «DOMContentLoaded» объекта «window», но defer намного короче), либо перед закрывающим тегом «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) Первым делом объявляем переменные для файлоприемника, инпута и файла (последний не инициализируем, поскольку его значение зависит от способа передачи — через клик по инпуту или бросание (drop) в файлоприемник): const dropZone = D.querySelector('div')
const input = D.querySelector('input') let file Отключаем обработку событий «dragover» и «drop» браузером: 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 и других ресурсах, сводятся либо к конвертации в другие форматы с помощью различных библиотек, либо к использованию viewer'ов от Google и Microsoft, которые не хотят работать с файловой системой и localhost. Вместе с тем, тип 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>` } Last, but not least, функция обработки 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> Для стилизации используется Materialize. Добавляем парочку собственных стилей: body {
max-width: 512px; margin: 0 auto; text-align: center; } input { text-align: center; } .get-data { margin-left: 1em; } Получаем следующее: JSON-файлы имеют следующий формат: {
"ключ": "значение", "ключ": "значение", ... } Нечетные инпуты с типом «text» — ключи, четные — значения. Присваиваем инпутам значения по умолчанию (значения могут быть любыми). Кнопка с классом «create-json» служит для получения значений, введенных пользователем, и создания файла. Кнопка с классов «get-data» — для получения данных. Переходим к скрипту: // находим кнопку с классом "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. Мы не используем атрибуты «required», поскольку будем валидировать форму в JS (с required поведение формы, состоящей из нескольких обязательных полей, становится раздражающим). Добавляем парочку собственных стилей: 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, за исключением того, что мы убрали значения по умолчанию для инпутов и передаем аргумент «name» в качестве значения одноименного атрибута (данный атрибут должен быть уникальным для каждого вопроса — это дает возможность переключать варианты ответов, выбирать один из нескольких). Значением name по умолчанию является время в миллисекундах, прошедшее с 1 января 1970 года, — простая альтернатива генераторам случайных значений типа Nanoid, используемых для получения уникального идентификатора (вряд ли пользователь успеет создать два вопроса за 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 или 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, регистрируемся, нажимаем на Workers справа, затем на кнопку «Create a Worker». Меняем название воркера на «data» (это не обязательно). В поле "{} 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/').then(...). Длинная статья получилась. Надеюсь, Вы нашли в ней для себя что-то полезное. Благодарю за внимание. =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_razrabotka_vebsajtov ( Разработка веб-сайтов ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:43
Часовой пояс: UTC + 5