【Vue3でwebアプリをつくろう2】ファイルを分割する(Vue CLIを使わずにコンポーネントを外部ファイル化)

これまでJavaScriptを用いて教師が生徒に対して英語のトレーニングを行うためのWebアプリのコード例をいくつか示してきました。

今回は定番の単語クイズを作ってみましょう。プログラミング初心者向けになるべく詳しく説明していきます。

データの用意

まずは,単語クイズのためのデータを用意しましょう。今回はサンプルとして28個分のデータを用意しました。データは以下のアドレスからダウンロードすることができます。

word001.zip

[{  "en_word": "while",
    "ja_word": "~の間",
    "pos": "接続詞",
    "en_sentence": "A computer connected to sensors on my head was able to record my brain waves while I was asleep.",
    "ja_sentence": "頭部のセンサーに接続されたコンピューターは私が寝ている間の脳波を記録することができました。"},
{   "en_word": "following",
    "ja_word": "以下の",
    "pos": "形容詞",
    "en_sentence": "We should consider the following question: Is it good or bad for students to work part-time?",
    "ja_sentence": "次のような質問を考えてみよう。学生がアルバイトをするのは悪いことは良いことか、それとも悪いことか。"},
{   "en_word": "human",
    "ja_word": "人間",
    "pos": "名詞",
    "en_sentence": "Human activities like the burning of forests and fossil fuels have produced an excess of gases such as carbon dioxide.",
    "ja_sentence": "森林や化石燃料を燃やすような人間の活動は、二酸化炭素などの過剰なガスを生み出した。"},

・・・・・・

]

データはJSON形式で構成されています。データは順番に,英単語,日本語,品詞,例文,例文の日本語訳の順番に並んでいます。

データはこのように,キーのセットで記述します。最初の行で言えば,キーはen_wordで,値はwhileです。あとでコードの中でこのキーを手がかりに値を取り出していきます。

データはJSON形式の他にCSV形式があります。データを作成する上ではCSV形式の方がはるかに楽ですが,JavaScriptでデータを扱うにはJSON形式の方がトラブルが少ないでしょう。

通常,上のようなJSON形式のデータをいちいちテキストエディタで書いたりはしません。ExcelなどでCSV形式のデータとして作成した上で,PythonでJSON形式に変換するのがベターです。しかしながら,それはそれでハードルの上がる話なので,とりあえずテキストエディタでデータを用意して構わないでしょう。

ヘッダー部

データを用意した上でコードを動かしていきましょう。まず,htmlのヘッダー部からです。

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>fasgram 英文法トレーニングwebアプリ</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
    <link rel="stylesheet" href="./fasgram.css">
</head>

コードを順番に見ていきましょう。

    <meta name="viewport" content="width=device-width,initial-scale=1">

ビューポートの設定です。ビューポートはスマートフォンで文字を適切な大きさで表示するために必要です。これがないと,スマートフォン上で文字が勝手に調節されて小さな文字になってしまうので,とりあえず書いておきましょう。

ヘッダーではコード上で用いる様々な外部機能を読み込みます。ここではDOM操作のための jquery,jquery UI,外部フォントを使うための Google Fonts,アイコンを表示するための Font Awesome を読み込んでいます。これらの機能は必ずしも必要なものではありません。

jqueryは主に画面に変更を加える(DOM操作と言います)際に必要な指令を短いコードで実現するための機能です。本来のJavaScriptのコードと記法が異なるため新たな学習が必要になりますが,覚えるとコードを書く手間が省けるので便利です。

Google Fonts は必ずしも必要ではありません。そもそも日本語のフォントを外部で読み込むと非常に大きなデータを読み込むことになるので,この方法はしばしば推奨されません。しかしながら,外部フォントを用いると,WindowsやMac,スマホ,タブレットなど様々な端末間で画面の表示を統一できるというメリットがあります。今回のアプリのようにある程度文字の表示品質が要求される場面では外部フォントを用いても良いでしょう。

アイコンフォントのFont Awersomeは多くのサイトで使用されていますが,残念ながら読み込み速度に問題を抱えているため,使用すべきかどうか迷うところです。ここで作成しているWebアプリではメリットが乏しいのですが,とりあえず一般的な方法に従っています。

body部

<body>
    <div class="container">
        <header>
            <p class="size-4">fasgram 英文法トレーニングwebアプリ<span class="badge">動作サンプル</span></p>
        </header>
        <article>
            <div id="display"></div>
        </article>
    </div>

body部はシンプルです。ブロック要素として<div id="display"></div>を記述しています。あとは,JavaScriptのコードでこの部分を書き換えてアプリを実行していきます。

読み上げ機能

JavaScriptのコードを説明していきます。

<script>
//読み上げ機能のインスタンス
var uttr = new SpeechSynthesisUtterance();
var voices = window.speechSynthesis.getVoices();

アプリでは機械音声によって英文を読み上げます。new SpeechSynthesisUtterance()で音声の再生機能のインスタンスを行います。インスタンスは実体化と訳されますが,これは音声読み上げ機能の電源スイッチをオンにする操作と解釈することができます。インスタンスした機能はオブジェクトuttrに代入されます。

ここで作ったuttrは,いわば一つのロボットです。uttrには,様々な設定を送り,音声を再生する命令を送ることができます。

window.speechSynthesis.getVoices()は,ブラウザが再生できる音声の種類を配列として返します。このコードは必ずしも必要なものではないようですが,初めに実行しておいたほうがトラブルが減るようなのでとりあえず記述しておきます。

main()

async function main() {
    var texts = await load_texts('./data/word001.json'); //問題文の読み込み
    for(i = 0; i < 10; i++) {
        const k = Math.floor(Math.random() * texts.length);
        await word_quiz(k, texts);  //ランダムに問題を呼び出す
    }
}

単語クイズを順次実行します。

ここでは,asyncawait などの機能を使用します。詳しい説明は省略しますが,コードを直鎖状に実行するために必要なものです。awaitを用いることで,一つの問題が終了してから次の問題に移るという処理が実現できます。

    var texts = await load_texts('./data/word001.json'); //問題文の読み込み

JSONのデータを読み込み,配列textsに格納します。ここで用意した関数load_textsはあとで記述しますが,内容については以前の記事を参照してください。

    for(i = 0; i < 10; i++) {
        const k = Math.floor(Math.random() * texts.length);
        await word_quiz(k, texts);  //ランダムに問題を呼び出す
    }

ここでは,単語クイズを10回行うことにします。出題する問題はデータの中からランダムに選びます。

Math.random()は0から1の間で乱数を発生させます。texts.lengthは配列の大きさを表します。今回はデータを28個用意したので,texts.length=28となります。

結果として,変数kには0から27のいずれかの値がランダムに返ってきます。

word_quiz()には,問題の番号kと問題文全体textsを与えます。awaitを付加することで,一つの問題が終了してから次の問題に移るようにします。

選択肢を自動的に選ぶ

async function word_quiz(k, texts) {
    let clicked;
    const text = texts[k];

main()から呼び出された関数word_quiz()に移ります。オブジェクトtextにはkで指定された番号の問題などが格納されます。例えばk=2のとき,textは以下のようになっています。

text = {
    "en_word": "human",
    "ja_word": "人間",
    "pos": "名詞",
    "en_sentence": "Human activities like the burning of forests and fossil fuels have produced an excess of gases such as carbon dioxide.",
    "ja_sentence": "森林や化石燃料を燃やすような人間の活動は、二酸化炭素などの過剰なガスを生み出した。"
}

このようなデータの集合をJavaScriptではオブジェクトと言います。実際にはオブジェクトはもっと複雑な構造を作ることができますが,ここでは最もシンプルな形で用いています。

    const answer = text.ja_word;

文字列answerに正解である文字列を格納します。上のデータで言えば,text.ja_word="人間"なので,answer="人間"となります。

    let option = Array(4);

今回は単語クイズの選択肢を4つ用意します。この数は自由に変更できます。

    option[0] = text.ja_word;

選択肢の一つ目に,正解の文字列を格納します。

    let option_all = option[0];

同様に文字列option_allに正解の文字列を格納します。この文字列は少し変わった仕事をします。

ここから残り3つの選択肢を作ります。選択肢は予め用意するのではなく,他の単語から拝借することで手間を省くことにします。

まず問題全体から日本語ja_wordをランダムに1つ選びます。このとき,選ばれた単語の品詞が正解の単語の品詞と一致するかどうかを判定します。上のデータで言えば,品詞は名詞なので,ランダムに選ばれた単語の品詞も名詞であることが条件です。そして,日本語の文字列が他の選択肢と重複しないことをもう一つの条件とします。

コードを見てみましょう。

    for(let i = 0; i < option.length - 1; i++) {

選択肢の初めの1個には正解の文字列を格納しています。よって,他の選択肢を決定するために選択肢の数より1つ少ない数だけ繰り返し処理します。ここでは選択肢を4個としていたので,3回繰り返し処理します。

.lengthは配列の要素の数を数えるものです。上で作った配列optionは要素の数が4個だったので,option.length=4です。

        for(let j = 0; j < 10; j++) {

ここからランダムに選択肢を選んでいきますが,選ばれた選択肢が上の条件を満たさない場合,再びランダムに選びなおすことになります。しかし,場合によっては何度繰り返しても条件に当てはまらないことがあるので,選びなおしは10回までに留めることにします。

            let a = Math.floor(Math.random() * texts.length);
            option[i+1] = texts[a].ja_word;

random()で問題の番号をランダムに決めます。option[0]はすでに正解の文字列が格納されているので,option[1]以降に他の日本語の文字列をいったん格納します。

            if(text.pos == texts[a].pos && option_all.indexOf(option[i+1]) == -1) {
                option_all += option[i+1];
                break;
            }

条件判定を行います。

text.pos == texts[a].posは品詞が一致しているかどうかを判定します。text.posは出題されている単語の品詞で,texts[a].posはランダムに選ばれた単語の品詞です。

.indexOf()は文字列を検索し,一致した文字の位置を返します。もし,一致するものが見つからなかった場合は-1を返します。ここでは,文字列option_allの中にランダムに選ばれた日本語の文字列が含まれるかどうかを判定しています。

option_allは選ばれた日本語の文字列の集合です。たとえば,4つ目の選択肢を作るときに,3つ目までの選択肢が,option[0]="人間"option[1]="部分"option[2]="大学"となっていたら,option_all="人間部分大学"となっています。ランダムに選ばれた日本語がこの文字列の中にあるかどうかを検索することで,他の選択肢と重複しないようにしているのです。

結果的にtext.pos == texts[a].pos && option_all.indexOf(option[i+1]) == -1という条件式が意味するのは,「出題する単語の品詞とランダムに選ばれた単語の品詞が一致する,かつ,他の選択肢の日本語と一致しない」という意味になります。

こうして条件を満たしたら,option_allに文字列を追加し,breakでループを抜けます。

選択肢のシャッフル

    //選択肢のシャッフル
    for(let i = 0; i < 100; i++) {
        let a = Math.floor(Math.random() * option.length);
        let b = Math.floor(Math.random() * option.length);
        [option[a], option[b]] = [option[b], option[a]];
    }

選択肢をシャッフルします。ここでは,2つの選択肢abをランダムに選び,お互いを入れ替える処理を100回繰り返すことによってシャッフルを行っています。

画面の初期化

    let content = '<p id="instruction" class="size-4 color-gray">'
        + '単語の意味を選択肢より選びなさい。'
        + '</p>'
        + '<p id="en-sentence" class="size-2"></p>'
        + '<p id="ja-sentence" class="size-3"></p>'
        + '<p id="en-word" class="size-1"></p>'
        + '<div id="speech_sentence"></div>'
        + '<div id="console"></div>'
        + '<p id="comment-box" class="size-3"></p>'
        + '<div id="btn-confirm"></div>';
    $('#display').html(content);

画面に要素を書き込んでいきます。ここでは,ブロック要素だけを用意して,実際の中身は後から書き込むことにします。それぞれのブロックにidを指定することで,その要素に文字を表示したり,消したりといった操作を行っていきます。

instructionは指示文,en-sentenceは問題に答えたあとに表示される英語の例文,ja-sentenceは例文の日本語訳,en-wordは問題として表示される英単語,speech_sentenceは「英文をクリックして音声を聞きましょう。」のメッセージ,consoleは選択肢,comment-boxは正解・不正解,btn-confirmは「次に進む」ボタンを表示する部分です。

これらをいったん文字列contentに格納して,初めにhtmlの中で書いた<div id="display"></div>の部分に.html()で書き込みます。

これをDOM操作と言います。これによって,もともとのhtmlコードが書き換えられることになります。

<div id="display"></div>

$('#display').html(content)  -->

<div id="display">
  <p id="instruction" class="size-4 color-gray">単語の意味を選択肢より選びなさい。</p>
  <p id="en-sentence" class="size-2"></p>
  <p id="ja-sentence" class="size-3"></p>
  <p id="en-word" class="size-1"></p>
  <div id="speech_sentence"></div>
  <div id="console"></div>
  <p id="comment-box" class="size-3"></p>
  <div id="btn-confirm"></div>
</div>

具体的なイメージは上のようになります。DOM操作によってブロック要素の中に新たなhtmlコードが書き込まれ,画面上に反映されます。

    $('#en-sentence,#ja-sentence').hide();

ここで,en-sentenceja-sentenceは問題に解答したあとに表示される部分なので,.hide()でいったん非表示にします。jqueryではこのようにコンマを使って複数の要素に同時に指示を与えることができます。

    $('#en-word').text(text.en_word);

英単語を表示します。

    set_speech_sentence('#speech_sentence',
            '#en-word',
            text.en_word);//読み上げ機能の設置

英単語の読み上げ機能を設置します。関数set_speech_sentenceはあとで定義しますが,引数にメッセージを表示するセレクタ,クリックさせたいセレクタ,英単語を与えます。

    for(let i = 0; i < option.length; i++) {
        content = '<button type="button"'
            + ' id="option' + i + '"'
            + ' class="btn-option size-1"'
            + ' value="' + option[i] + '">'
            + option[i]
            + '</button>'
        $('#console').append(content);
    }

選択肢のボタンを表示します。valueに選択肢の日本語を与え,あとからボタンがクリックされたときに正解・不正解を判断するために使います。

ボタンクリック時の処理

    await new Promise((resolve) => {
        for(let i = 0; i < option.length; i++) {
            $('#option'+i).on('click', function() {
                clicked = $(this).val();
                resolve();
            });
        }
    });

ボタンクリック時の処理です。ここで,Promise()resolve()の組み合わせを用います。このように書くことで,resolve()が実行されるまで処理がストップします。

for文によって,選択肢のボタンの数だけイベントリスナーを設置していきます。イベントリスナーは画面上で何かの出来事が起こったときに処理を行う仕組みです。ここでは,.on('click')でボタンがクリックされたときの処理を記述しています。

選択肢のボタンにはそれぞれid="option0"id="option3"が設定されていて,i=0のとき,$('#option'+i)$('#option0')となります。

                clicked = $(this).val();

文字列clickedにボタンのvalueを格納します。上で説明したようにvalueには選択肢として表示されている日本語の文字列が格納されています。こうしてclickedには押されたボタンの日本語が格納され,正解・不正解の判定に使用されます。

また,$(this)は一つ上の行の'#option'+iを指しています。

正解・不正解の判定

    //英文と日本文を表示
    $('#instruction,#en-word,#console').remove();
    $('#en-sentence').text(text.en_sentence).show();
    $('#ja-sentence').text(text.ja_sentence).show();

正解・不正解の判定に入ります。ここで,問題の指示文#instruction,英単語#en-word,選択肢#consoleは必要ないので,.remove()で画面上から消去します。

そして,初めにいったん見えないようにしていた例文#en-sentence,日本語訳#ja-sentencetext()で文字を書き込み,.show()で再び表示します。上のように,複数の処理を連続して書くことができます。

    if(clicked == answer) {
        //正解の場合の処理
        content = '<p><i class="fas fa-check color-red"></i>'
            +' 正解</p>';
        $('#comment-box').html(content);

クリックされた選択肢と正解が一致した場合,正解の処理に移ります。今回はスコアを加算するなどの処理は行わず,単に正解であることを表示するだけです。

<i class="fas fa-check color-red"></i>はFont Awesomeのコードで,アイコンを表示します。

    } else {
        //不正解の処理
        content = '<p><i class="fas fa-times-circle color-red"></i>'
            + ' 不正解</p>'
            + '<p>正解は '
            + answer
            + '</p>';
        $('#comment-box').html(content);
    }

不正解の場合の処理です。同様に,単に不正解であることをメッセージとして表示するだけです。

後処理

    set_speech_sentence('#speech_sentence',
            '#en-sentence',
            text.en_sentence);//読み上げ機能の設置
    speech_sentence(text.en_sentence);
    await button_confirm('#btn-confirm');
    speechSynthesis.cancel();
    $(document).off(); //イベントリスナーの解除
    return true;

問題文を表示したときと同様,関数set_speech_sentence()で読み上げ機能を設置します。これによって,例文をクリックしたときに英文が読み上げられます。また,関数speech_sentence()は直接英文を読み上げる機能です。これによって,例文が表示されると同時に英文が読み上げられます。

await button_confirm('#btn-confirm');

関数button_confirm()は「次に進む」ボタンを表示します。引数にはボタンを表示するセレクタを指定します。

関数の前にawaitを記述すると関数の中でresolve()が実行されるまで処理をストップすることができます。このようにしないと,プログラムはボタンが押される前に勝手に次の問題に進んでしまうでしょう。

コードを見ると分かりますが,awaitを記述する関数にはasyncを記述する必要があります。こうして,asyncawaitPromise()resolve()をセットにすることで,ボタンがクリックされたら次に進むという処理が実現できます。

この辺りは,プログラミング初心者にとっては非常に理解が困難な部分かもしれません。まずは,コードを真似して実際にコードを動かしながら少しずつ理解を深めていくと良いでしょう。

    speechSynthesis.cancel();

「次に進む」ボタンがクリックされたときに例文が読み上げの途中であれば,その読み上げを中止します。

    $(document).off(); //イベントリスナーの解除

最後にボタンに設定されたイベントリスナーを解除します。この解除を行わないと次の問題に進んだときにイベントリスナーが上書きされ,1回ボタンをクリックするとボタンが2回クリックされたことになり処理がおかしくなります。不要となったイベントリスナーは解除しておきます。

その他の関数

その他の関数については説明を省略します。以前の記事で説明されています。

//次に進むボタン 引数 selector ボタンを格納するセレクタ
async function button_confirm(selector) {
    const content = '<div class="btn-wrapper">'
        + '<button type="button" id="confirm"'
        + ' class="btn-confirm size-3">'
        + '<i class="fas fa-caret-right"></i>'
        + ' 次に進む'
        + '</button>'
        + '</div>';
    $(selector).html(content);
    await new Promise((resolve) => {
        $('#confirm').on('click', function() {
            resolve();
        })
        $(document).on('keydown', function(e) {
            if(e.keyCode == 13) {
                resolve();
            }
        }); 
    });
    $(document).off(); //イベントリスナーの解除
}
function set_speech_sentence(selector1, selector2, en_sentence) {
    //メッセージの表示
    const content = '<p class="color-gray size-4">'
        + '<i class="fas fa-volume-up"></i>'
        + '英文をクリックして音声を聞きましょう。</p>';
    $(selector1).html(content)
    //読み上げ機能の設置
    $(selector2).on('click',function() {
        $(this).effect("highlight", { color: "#ccdeff" }, 2000);
        speech_sentence(en_sentence);
    });
}
//音声読み上げ機能 引数 sentence 読み上げる英文の文字列
function speech_sentence(sentence) {
        speechSynthesis.cancel();
        const voices = speechSynthesis.getVoices();
        uttr.voice = voices[3]; //女性の声
        //this.uttr.voice = voices[5]; //男性の声
        this.uttr.volume = 1.0; //音量
        this.uttr.rate = 0.9; //読み上げ速度
        this.uttr.pitch = 1.0; //音程
        this.uttr.lang = 'en-US'; //言語
        this.uttr.text = sentence; //読み上げテキスト
        speechSynthesis.speak(this.uttr); //再生
}
//問題文の読み込み
async function load_texts(URL) {
    const data = await fetch(URL, {cache: "no-cache"});
    const obj = await data.json();
    const obj_json = JSON.stringify(obj); //json文字列に変換
    const texts = JSON.parse(obj_json); //配列に格納
    return texts;
}

まとめ

この記事では,単語クイズを行うwebアプリの作成方法について学びました。JSON形式のデータを用意し,それを利用してクイズの選択肢を自動的に生成するアルゴリズムが紹介されました。また,イベントリスナーにPromise()を組み合わせることで直鎖状に処理を行う方法を学びました。これらの方法を応用することで,様々なクイズ形式のwebアプリを作成することができます。

最後にコード全体を示します。コードはオープンソースであり,著作権者の許諾なしに自由に改変・再配布できます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>fasgram 英文法トレーニングwebアプリ</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
    <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
    <link rel="stylesheet" href="./fasgram.css">
</head>
<body>
    <div class="container">
        <header>
            <p class="size-4">fasgram 英文法トレーニングwebアプリ<span class="badge">動作サンプル</span></p>
        </header>
        <article>
            <div id="display"></div>
        </article>
    </div>
<script>
//読み上げ機能のインスタンス
var uttr = new SpeechSynthesisUtterance();
var voices = window.speechSynthesis.getVoices();
async function main() {
    var texts = await load_texts('./data/word001.json'); //問題文の読み込み
    for(i = 0; i < 10; i++) {
        const k = Math.floor(Math.random() * texts.length);
        await word_quiz(k, texts);  //ランダムに問題を呼び出す
    }
}
//単語クイズ
async function word_quiz(k, texts) {
    let clicked;
    const text = texts[k];
    //選択肢の抽出
    const answer = text.ja_word;
    let option = Array(4);
    option[0] = text.ja_word;
    let option_all = option[0];
    for(let i = 0; i < option.length - 1; i++) {
        for(let j = 0; j < 10; j++) {
            let a = Math.floor(Math.random() * texts.length);
            option[i+1] = texts[a].ja_word;
            if(text.pos == texts[a].pos && option_all.indexOf(option[i+1]) == -1) {
                option_all += option[i+1];
                break;
            }
        }
    }
    //選択肢のシャッフル
    for(let i = 0; i < 100; i++) {
        let a = Math.floor(Math.random() * option.length);
        let b = Math.floor(Math.random() * option.length);
        [option[a], option[b]] = [option[b], option[a]];
    }
    //画面の初期化
    let content = '<p id="instruction" class="size-4 color-gray">'
        + '単語の意味を選択肢より選びなさい。'
        + '</p>'
        + '<p id="en-sentence" class="size-2"></p>'
        + '<p id="ja-sentence" class="size-3"></p>'
        + '<p id="en-word" class="size-1"></p>'
        + '<div id="speech_sentence"></div>'
        + '<div id="console"></div>'
        + '<p id="comment-box" class="size-3"></p>'
        + '<div id="btn-confirm"></div>';
    $('#display').html(content);
    $('#en-sentence,#ja-sentence').hide();
    $('#en-word').text(text.en_word);
    set_speech_sentence('#speech_sentence',
            '#en-word',
            text.en_word);//読み上げ機能の設置
    //選択肢のボタンを表示
    for(let i = 0; i < option.length; i++) {
        content = '<button type="button"'
            + ' id="option' + i + '"'
            + ' class="btn-option size-1"'
            + ' value="' + option[i] + '">'
            + option[i]
            + '</button>'
        $('#console').append(content);
    }
    //選択肢がクリックされたときの処理
    await new Promise((resolve) => {
        for(let i = 0; i < option.length; i++) {
            $('#option'+i).on('click', function() {
                clicked = $(this).val();
                resolve();
            });
        }
    });
    //英文と日本文を表示
    $('#instruction,#en-word,#console').remove();
    $('#en-sentence').text(text.en_sentence).show();
    $('#ja-sentence').text(text.ja_sentence).show();
    //正解不正解の判定
    if(clicked == answer) {
        //正解の場合の処理
        content = '<p><i class="fas fa-check color-red"></i>'
            +' 正解</p>';
        $('#comment-box').html(content);
    } else {
        //不正解の処理
        content = '<p><i class="fas fa-times-circle color-red"></i>'
            + ' 不正解</p>'
            + '<p>正解は '
            + answer
            + '</p>';
        $('#comment-box').html(content);
    }
    //「次に進む」ボタンの表示
    set_speech_sentence('#speech_sentence',
            '#en-sentence',
            text.en_sentence);//読み上げ機能の設置
    speech_sentence(text.en_sentence);
    await button_confirm('#btn-confirm');
    speechSynthesis.cancel();
    $(document).off(); //イベントリスナーの解除
    return true;
}
//次に進むボタン 引数 selector ボタンを格納するセレクタ
async function button_confirm(selector) {
    const content = '<div class="btn-wrapper">'
        + '<button type="button" id="confirm"'
        + ' class="btn-confirm size-3">'
        + '<i class="fas fa-caret-right"></i>'
        + ' 次に進む'
        + '</button>'
        + '</div>';
    $(selector).html(content);
    await new Promise((resolve) => {
        $('#confirm').on('click', function() {
            resolve();
        })
        $(document).on('keydown', function(e) {
            if(e.keyCode == 13) {
                resolve();
            }
        }); 
    });
    $(document).off(); //イベントリスナーの解除
}
function set_speech_sentence(selector1, selector2, en_sentence) {
    //メッセージの表示
    const content = '<p class="color-gray size-4">'
        + '<i class="fas fa-volume-up"></i>'
        + '英文をクリックして音声を聞きましょう。</p>';
    $(selector1).html(content)
    //読み上げ機能の設置
    $(selector2).on('click',function() {
        $(this).effect("highlight", { color: "#ccdeff" }, 2000);
        speech_sentence(en_sentence);
    });
}
//音声読み上げ機能 引数 sentence 読み上げる英文の文字列
function speech_sentence(sentence) {
        speechSynthesis.cancel();
        const voices = speechSynthesis.getVoices();
        uttr.voice = voices[3]; //女性の声
        //this.uttr.voice = voices[5]; //男性の声
        this.uttr.volume = 1.0; //音量
        this.uttr.rate = 0.9; //読み上げ速度
        this.uttr.pitch = 1.0; //音程
        this.uttr.lang = 'en-US'; //言語
        this.uttr.text = sentence; //読み上げテキスト
        speechSynthesis.speak(this.uttr); //再生
}
//問題文の読み込み
async function load_texts(URL) {
    const data = await fetch(URL, {cache: "no-cache"});
    const obj = await data.json();
    const obj_json = JSON.stringify(obj); //json文字列に変換
    const texts = JSON.parse(obj_json); //配列に格納
    return texts;
}
main();
</script>
</body>
</html>
/*スマートフォン用の基準文字サイズ*/
body {
    font-family: 'Noto Serif', 'Kosugi', serif;
    font-size: 24px;
    background-color: #fafafa;
}
/*コンテナの幅*/
.container {
    max-width: 680px;
}    
/*デスクトップ用の基準文字サイズ*/
@media screen and (min-width:680px) {
    body {
        display: flex;
        justify-content: center;
        font-size: 18px;
    }
    /*コンテナの幅*/
    .container {
        width: 680px;
    }
}
#controll-panel {
    display: flex;
    justify-content: space-between;
}
#display {
    width: auto;
    background-color: raba(256,256,256,0.2);
}
p {
    margin: 0.5rem;
}
span {
    display: inline-block;
}
/*フォントサイズ*/
.size-1 {
    font-size: 1.5rem;
    font-weight: 400;
}
.size-2 {
    font-size: 1.2rem;
    font-weight: 400;
}
.size-3 {
    font-size: 1.0rem;
    font-weight: 400;
}
.size-4 {
    font-size: 0.75rem;
}
.bold {
    font-weight: 700;
}
/*文字色*/
.color-blue {
    color: #3366CC;
}
.color-red {
    color: #FF6600;
}
.color-gray {
    color: #808080;
}
/*ボタンwrapper*/
.btn-wrapper {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
    width : auto;
    margin-top: 2rem;
}
.btn-wrapper-right {
    display: flex;
    justify-content: flex-end;
    width: auto;
}
/*スタートボタン*/
.btn-start {
    font-weight: 700;
    padding: 1rem 4rem;
    text-align: center;
    vertical-align: middle;
    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
    outline: none; /*ボタンを押したときの枠線を消す*/
    color: #3366cc;
    background-color: #fff;
    border: 1px solid #ddd;
    letter-spacing: 0.25rem;
    border-radius: 0.5rem;
}
/*次に進むボタン*/
.btn-confirm {
    font-weight: 800;
    padding: 1rem 2rem;
    text-align: center;
    vertical-align: middle;
    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
    outline: none; /*ボタンを押したときの枠線を消す*/
    color: #3366cc;
    background-color: #fff;
    border: 1px solid #ddd;
    border-radius: 0.5rem;
}
/*最初に戻るボタン*/
.btn-home {
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 5px 10px;;
    background: #f1e767;
    background: -webkit-gradient(linear, left top, left bottom, from(#fdfbfb), to(#ebedee));
    background: -webkit-linear-gradient(top, #fdfbfb 0%, #ebedee 100%);
    background: linear-gradient(to bottom, #fdfbfb 0%, #ebedee 100%);
    -webkit-box-shadow: inset 1px 1px 1px #fff;
    box-shadow: inset 1px 1px 1px #fff;
    outline: none;
}
/*ソフトウェアキーボードのwrapper*/
.keyboard {
    display: flex;
    justify-content: center;
}
/*ソフトウェアキーボードのキー*/
.btn-key {
    width: 9%;
    margin: 1px;
    padding: 0.75rem 0rem;
    border: 1px solid #ddd;
    border-radius: 0.5rem;
    background-color: #fff;
    font-weight: 700;
    font-size: 1rem;
    box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08);
    outline: none;
}
/*選択肢ボタン*/
.btn-option {
    margin: 5px 2px;
    padding: 0.75rem 1.25rem;
    border: 1px solid #ccc;
    border-radius: 0.5rem;
    background: #fff;
    box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.16);
    outline: none;
}
.btn-half {
    width: 45%;
}
/*バッジ*/
.badge {
    padding: 2px 5px;
    margin-left: 1px;
    font-size: 75%;
    color: white;
    border-radius: 6px;
    box-shadow: 0 0 3px #ddd;
    white-space: nowrap;
    background-color: #58ACFA;
}
#en-sentence {
    margin: 5px 2px;
    padding: 1.8rem 1.5rem;
    border: 1px solid #ccc;
    border-radius: 0.5em;
    background: #fff;
    box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.16);
    outline: none;
}
#en-word {
    margin: 5px 2px;
    padding: 1.8rem 1.5rem;
    border: 1px solid #ccc;
    border-radius: 0.5em;
    background: #fff;
    box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.16);
    outline: none;
    text-align: center;
}
#description {
    margin: 5px 2px;
    padding: 1.5rem 1.5rem;
    border: 1px solid #ccc;
    border-radius: 0.5em;
    background: #fff;
    box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.16);
    outline: none;    
}
#new-item {
    display: flex;
    justify-content: center;
    margin: 3rem;
}