Вчера мы показали на сайте простую игрульку — тест по выбору языка программирования. Несмотря на его внешнюю несерьёзность, под капотом у него довольно хитрый механизм. Рассказываем, как это устроено и как вам сделать такое же.
В чём идея
В одном из пабликов мы увидели картинку о выборе языка программирования и решили сделать такой же, но в формате «Кода». Суть проста: по очереди отвечаем на вопросы, каждый из которых ведёт нас по своему пути, и в итоге получаем какой-то ответ. Картинка была вроде такой, специально показываем мелко, чтобы не вникать в сами вопросы:
Ограничения проекта
Если бы это была отдельная страница на нашем учебном сервере, всё было бы просто: написали отдельно страницу и скрипт — и тест готов. Но задача была в том, чтобы встроить тест в стандартную страницу Вордпресса, на котором работает сайт журнала «Код».
В Вордпрессе страница собирается в тот момент, когда пользователь запрашивает её с сервера, а до этого она лежит в виде отдельных модулей: заголовка, подвала, обвеса с похожими статьями и другими штуками. Мы не можем зайти в файл стилей и добавить в него нужное нам оформление только ради одной статьи — это будет неряшливо и непрактично. Нужно придумать такое решение, которое добавит на страницу нужные стили и не затронет остальные статьи.
По этой же причине мы не хотим подключать отдельно файл со скриптом — весь код надо будет описать прямо в теле страницы. Это тоже нужно учесть при работе над проектом.
А ещё нам нельзя показывать сразу все вопросы и варианты ответов, иначе пропадёт интрига. Значит, нам нужно решить, как добавлять вопросы на страницу прямо по ходу теста. Стандартный способ «разместить заранее блок с вопросом и скрыть его» нам не подходит: скрытый блок всё равно занимает место на странице и вёрстка поплывёт. А ещё в этом случае нужно будет удалять неиспользуемые вопросы, что тоже увеличивает сложность проекта.
Что делаем
Единственный инструмент, который нам подходит в Вордпрессе, — стандартный блок «HTML-код». В нём можно размещать любой код: система распознает все теги и добавит их на страницу так, как будто мы сделали это вручную.
Если учесть все ограничения, то получится, что нам нужен в этом блоке такое:
- HTML-код теста, имитирующий поведение Вордпресса;
- скрипт, который добавит на страницу все вопросы в нужной последовательности;
- пустые блоки <div>, куда скрипт отправит текст вопросов с вариантами ответа.
Пишем HTML-код
Так как Вордпресс — это просто конструктор, который работает со стандартными HTML-тегами, то для абзацев текста он тоже использует стандартный тег <p>. Зная это, мы можем добавить первый вопрос и две кнопки:
<p>Вы хотите много зарабатывать?</p>
<button id = 'a11' onclick='an11()'>Да</button>
<button id = 'a10' onclick='an10()'>Нет</button>
Так как нам нужно добавлять вопросы прямо по ходу теста, сразу предусмотрим для них место в блоках. Туда же добавим блок для вывода результата теста жирным начертанием. Плюс такого решения в том, что пустые блоки <div> не занимают место в вёрстке, поэтому нижняя плашка «Практикума» не уедет вниз:
<!-- второй вопрос -->
<div id = 'q2'></div>
<!-- третий вопрос -->
<div id = 'q3'></div>
<div id = 'q4'></div>
<div id = 'q5'></div>
<div id = 'q6'></div>
<!-- результат -->
<strong>
<div id = 'result'></div>
</strong>
Последнее, что нам осталось сделать, — добавить новой строчкой тег скрипта:
<script type="text/javascript"></script>
Он пока пустой, но мы сейчас это исправим.
Пишем скрипт
Так как нам нужно управлять почти всем, что мы добавили, сразу получим доступ к блокам и кнопкам:
// получаем доступ к результатам теста
var result = document.getElementById('result');
var result_text = document.getElementById('result_text');
// получаем доступ ко всем элементам с вопросами
var q2 = document.getElementById('q2');
var q3 = document.getElementById('q3');
var q4 = document.getElementById('q4');
var q5 = document.getElementById('q5');
var q6 = document.getElementById('q6');
// и доступ к первым кнопкам в тесте
var a11 = document.getElementById('a11');
var a10 = document.getElementById('a10');
А теперь тот приём, который мы будем использовать всё время:
- Находим элемент по его ID.
- С помощью свойства innerHTML записываем в него новое содержимое.
- Так как это всё выполняется в браузере, то, если мы запишем туда HTML-теги, браузер их сразу выполнит, как будто они уже были на странице.
- В итоге мы сможем создавать любые вопросы в нужном месте и управлять ходом теста.
Самое интересное, что с помощью этого же приёма мы можем сразу добавить в раздел <head> стили для кнопок (потому что напрямую изменить его в Вордпрессе мы не можем):
// ждём, пока документ полностью загрузится
document.addEventListener("DOMContentLoaded", function(event) {
// находим раздел <head> и добавляем в него тег со стилем для этой страницы
// в нём будет единственный стиль, который отвечает за внешний вид кнопок
document.getElementsByTagName('head')[0].innerHTML +="<style type='text/css'>button {background: transparent; color: #222; border-color: #4676d7; padding: 10px;margin-bottom: 20px; margin-left: 10px;}"
});
Обрабатываем нажатие на кнопки
Каждый выбор ответа приводит к тому, что в тесте появляется новый вопрос. Мы уже заранее прописали названия обработчиков нажатия на кнопки — an10 и an11. Настало время написать эти функции внутри скрипта:
// хочет много зарабатывать
function an11() {
// запрещаем дальнейшие нажатия на кнопки
a10.disabled=true;
a11.disabled=true;
a10.style.visibility='hidden';
// показываем следующий вопрос
q2.innerHTML ='<p>Вы часто совершаете ошибки или сомневаетесь в себе?</p><button id = \'a21\' onclick=\'an21()\'>Да</button><button id = \'a20\' onclick=\'an20()\'>Нет</button>'
}
// не хочет много зарабатывать
function an10() {
// запрещаем дальнейшие нажатия на кнопки
a10.disabled=true;
a11.disabled=true;
a11.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Delphi</p>';
}
Вот что мы только что сделали:
- Сделали две функции, каждая из которых привязана к своей кнопке на странице.
- Первая функция с цифрой 1 в конце отвечает за ответ «Да», а вторая, которая заканчивается на ноль, — за ответ «Нет».
- В каждой функции мы запретили дальнейшие нажатия на кнопки и оставили видимой ту, на которую нажали. Так будет виден путь, по которому мы придём к какому-то ответу.
- В конце первой функции мы обратились к пустому блоку для второго вопроса и добавили в него новые теги — абзаца с новым вопросом и двумя кнопками.
- В конце второй функции обратились к блоку с результатом и добавили в него ответ.
Смысл в том, чтобы на каждом этапе или выдавать ответ, или добавлять в следующий пустой блок новые теги с вопросом и кнопками. Единственное, что нам нужно заранее предусмотреть в скрипте, — это следить за названиями функций в кнопках (чтобы они были уникальные) и сразу добавлять в скрипт обработчики для них.
Например, мы только что добавили новый вопрос в блок q2. Это значит, что браузер выведет текст вопроса и добавит ниже две кнопки — «Да» и «Нет». К первой кнопке у нас привязан обработчик an21() (2 — номер вопроса, 1 — положительный ответ), а ко второй — обработчик an20() (2 — номер вопроса, 0 — отрицательный ответ). Это значит, что нам нужно сразу добавить в скрипт эти две функции и потом не забыть их наполнить.
В начале каждой функции нужно не забыть получить доступ к кнопкам через document.getElementById(). Мы не можем сделать это в самом начале скрипта, потому что на старте у нас всего две кнопки, а остальные мы добавляем динамически. Это значит, что браузер при запуске не увидит этих кнопок на странице и не даст к ним доступ, когда они появятся. Правило простое: добавляете новый контент — сразу получите к нему доступ.
Главная сложность такого теста — следить за тем, чтобы у нас срабатывали правильные обработчики, а текст добавлялся в нужные блоки. Если понять этот принцип, то тест можно делать каким угодно сложным, например, сделать 50 вопросов и к каждому дать по 4 варианта ответа.
Чтобы было понятнее, посмотрите полный код скрипта, где уже прописаны все обработчики и добавление новых вопросов:
// получаем доступ к результатам теста
var result = document.getElementById('result');
var result_text = document.getElementById('result_text');
// получаем доступ ко всем элементам с вопросами
var q2 = document.getElementById('q2');
var q3 = document.getElementById('q3');
var q4 = document.getElementById('q4');
var q5 = document.getElementById('q5');
var q6 = document.getElementById('q6');
// и доступ к первым кнопкам в тесте
var a11 = document.getElementById('a11');
var a10 = document.getElementById('a10');
// ждём, пока документ полностью загрузится
document.addEventListener("DOMContentLoaded", function(event) {
// находим раздел <head> и добавляем в него тег со стилем для этой страницы
// в нём будет единственный стиль, который отвечает за внешний вид кнопок
document.getElementsByTagName('head')[0].innerHTML +="<style type='text/css'>button {background: transparent; color: #222; border-color: #4676d7; padding: 10px;margin-bottom: 20px; margin-left: 10px;}"
});
// хочет много зарабатывать
function an11() {
// запрещаем дальнейшие нажатия на кнопки
a10.disabled=true;
a11.disabled=true;
a10.style.visibility='hidden';
// показываем следующий вопрос
q2.innerHTML ='<p>Вы часто совершаете ошибки или сомневаетесь в себе?</p><button id = \'a21\' onclick=\'an21()\'>Да</button><button id = \'a20\' onclick=\'an20()\'>Нет</button>'
}
// не хочет много зарабатывать
function an10() {
// запрещаем дальнейшие нажатия на кнопки
a10.disabled=true;
a11.disabled=true;
a11.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Delphi</p>';
}
// Часто сомневается
function an21() {
// получаем доступ к кнопкам
var a20 = document.getElementById('a20');
var a21 = document.getElementById('a21');
// запрещаем дальнейшие нажатия на кнопки
a20.disabled=true;
a21.disabled=true;
a20.style.visibility='hidden';
// показываем следующий вопрос
q3.innerHTML = '<p>Очень?</p> <button id = \'a31\' onclick=\'an31()\'>Да</button> <button id = \'a30\' onclick=\'an30()\'>Нет</button>';
}
// Не часто сомневается
function an20() {
// получаем доступ к кнопкам
var a21 = document.getElementById('a21');
var a20 = document.getElementById('a20');
// запрещаем дальнейшие нажатия на кнопки
a20.disabled=true;
a21.disabled=true;
a21.style.visibility='hidden';
// показываем следующий вопрос
q3.innerHTML = '<p>Вы инженер или математик?</p> <button id = \'a91\' onclick=\'an91()\'>Да</button> <button id = \'a90\' onclick=\'an90()\'>Нет</button>';
}
// Есть друзья
function an31() {
var a30 = document.getElementById('a30');
var a31 = document.getElementById('a31');
// запрещаем дальнейшие нажатия на кнопки
a31.disabled=true;
a30.disabled=true;
a30.style.visibility='hidden';
// показываем следующий вопрос
q4.innerHTML = '<p>У вас есть друзья?</p> <button id = \'a41\' onclick=\'an41()\'>Да</button> <button id = \'a40\' onclick=\'an40()\'>Нет</button>';
}
// Не часто сомневается
function an30() {
var a30 = document.getElementById('a30');
var a31 = document.getElementById('a31');
// запрещаем дальнейшие нажатия на кнопки
a30.disabled=true;
a31.disabled=true;
a31.style.visibility='hidden';
// показываем следующий вопрос
q4.innerHTML = '<p>Вы читаете каждую статью в «Коде»?</p> <button id = \'a61\' onclick=\'an61()\'>Да</button> <button id = \'a60\' onclick=\'an60()\'>Нет</button>';
}
// Друзья тоже тупят
function an41() {
var a41 = document.getElementById('a41');
var a40 = document.getElementById('a40');
// запрещаем дальнейшие нажатия на кнопки
a41.disabled=true;
a40.disabled=true;
a40.style.visibility='hidden';
// показываем следующий вопрос
q5.innerHTML = '<p>Они тоже часто совершают ошибки или сомневаются в себе?</p> <button id = \'a51\' onclick=\'an51()\'>Да</button> <button id = \'a50\' onclick=\'an50()\'>Нет</button>';
}
// Друзей нет
function an40() {
var a41 = document.getElementById('a41');
var a40 = document.getElementById('a40');
// запрещаем дальнейшие нажатия на кнопки
a41.disabled=true;
a40.disabled=true;
a41.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — PHP</p>';
}
// Друзья тоже тупят
function an51() {
var a51 = document.getElementById('a51');
var a50 = document.getElementById('a50');
// запрещаем дальнейшие нажатия на кнопки
a51.disabled=true;
a50.disabled=true;
a50.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — JavaScript</p>';
}
// Друзья не тупят
function an50() {
var a51 = document.getElementById('a51');
var a50 = document.getElementById('a50');
// запрещаем дальнейшие нажатия на кнопки
a51.disabled=true;
a50.disabled=true;
a51.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Ruby</p>';
}
// Читает каждую статью в Коде
function an61() {
var a61 = document.getElementById('a61');
var a60 = document.getElementById('a60');
// запрещаем дальнейшие нажатия на кнопки
a60.disabled=true;
a61.disabled=true;
a60.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Python</p>';
}
// Не читает каждую статью в Коде
function an60() {
var a61 = document.getElementById('a61');
var a60 = document.getElementById('a60');
// запрещаем дальнейшие нажатия на кнопки
a61.disabled=true;
a60.disabled=true;
a61.style.visibility='hidden';
// показываем следующий вопрос
q5.innerHTML = '<p>Вам нравится Windows?</p> <button id = \'a71\' onclick=\'an71()\'>Да</button> <button id = \'a70\' onclick=\'an70()\'>Нет</button>';
}
// Нравится Windows
function an71() {
var a71 = document.getElementById('a71');
var a70 = document.getElementById('a70');
// запрещаем дальнейшие нажатия на кнопки
a71.disabled=true;
a70.disabled=true;
a70.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — C#</p>';
}
// Не нравится Windows
function an70() {
var a71 = document.getElementById('a71');
var a70 = document.getElementById('a70');
// запрещаем дальнейшие нажатия на кнопки
a71.disabled=true;
a70.disabled=true;
a71.style.visibility='hidden';
// показываем следующий вопрос
q6.innerHTML = '<p>Любите хвататься своим умом?</p> <button id = \'a81\' onclick=\'an81()\'>Да</button> <button id = \'a80\' onclick=\'an80()\'>Нет</button>';
}
// Нравится Windows
function an81() {
var a81 = document.getElementById('a81');
var a80 = document.getElementById('a80');
// запрещаем дальнейшие нажатия на кнопки
a81.disabled=true;
a80.disabled=true;
a80.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Swift</p>';
}
// Не нравится Windows
function an80() {
var a81 = document.getElementById('a81');
var a80 = document.getElementById('a80');
// запрещаем дальнейшие нажатия на кнопки
a81.disabled=true;
a80.disabled=true;
a81.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Perl</p>';
}
// Инженер
function an91() {
var a91 = document.getElementById('a91');
var a90 = document.getElementById('a90');
// запрещаем дальнейшие нажатия на кнопки
a91.disabled=true;
a90.disabled=true;
a90.style.visibility='hidden';
// показываем следующий вопрос
q4.innerHTML = '<p>Любите старину?</p> <button id = \'a101\' onclick=\'an101()\'>Да</button> <button id = \'a100\' onclick=\'an100()\'>Нет</button>';
}
// Не инженер
function an90() {
var a91 = document.getElementById('a91');
var a90 = document.getElementById('a90');
// запрещаем дальнейшие нажатия на кнопки
a91.disabled=true;
a90.disabled=true;
a91.style.visibility='hidden';
// показываем следующий вопрос
q4.innerHTML = '<p>Вы интроверт?</p> <button id = \'a111\' onclick=\'an111()\'>Да</button> <button id = \'a110\' onclick=\'an110()\'>Нет</button>';
}
// Любит старину
function an101() {
var a101 = document.getElementById('a101');
var a100 = document.getElementById('a100');
// запрещаем дальнейшие нажатия на кнопки
a101.disabled=true;
a100.disabled=true;
a100.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Pascal</p>';
}
// Не любит старину
function an100() {
var a101 = document.getElementById('a101');
var a100 = document.getElementById('a100');
// запрещаем дальнейшие нажатия на кнопки
a101.disabled=true;
a100.disabled=true;
a101.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Scala</p>';
}
// Интроверт
function an111() {
var a111 = document.getElementById('a111');
var a110 = document.getElementById('a110');
// запрещаем дальнейшие нажатия на кнопки
a111.disabled=true;
a110.disabled=true;
a110.style.visibility='hidden';
// показываем следующий вопрос
q5.innerHTML = '<p>А хотите славы и внимания?</p> <button id = \'a121\' onclick=\'an121()\'>Да</button> <button id = \'a120\' onclick=\'an120()\'>Нет</button>';
}
// Не интроверт
function an110() {
var a111 = document.getElementById('a111');
var a110 = document.getElementById('a110');
// запрещаем дальнейшие нажатия на кнопки
a111.disabled=true;
a110.disabled=true;
a11.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — С</p>';
}
// Хочет славы
function an121() {
var a121 = document.getElementById('a121');
var a120 = document.getElementById('a120');
// запрещаем дальнейшие нажатия на кнопки
a121.disabled=true;
a120.disabled=true;
a120.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — Java</p>';
}
// Не хочет славы
function an120() {
var a121 = document.getElementById('a121');
var a120 = document.getElementById('a120');
// запрещаем дальнейшие нажатия на кнопки
a121.disabled=true;
a120.disabled=true;
a121.style.visibility='hidden';
// выводим результат
result.innerHTML = '<p>Ваш выбор — С++</p>';
}