mirror of
				https://github.com/ZetaKebab/japanese-conjugation-drill.git
				synced 2025-10-30 22:09:49 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			853 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			853 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| // drill.js
 | ||
| 
 | ||
| var transformations = [];
 | ||
| 
 | ||
| var log;
 | ||
| 
 | ||
| Array.prototype.randomElement = function () {
 | ||
|   return this[Math.floor(Math.random() * this.length)]
 | ||
| }
 | ||
| 
 | ||
| // From: http://stackoverflow.com/a/2897510
 | ||
| new function ($) {
 | ||
|   $.fn.getCursorPosition = function () {
 | ||
|     var input = this.get(0);
 | ||
|     if (!input) return; // No (input) element found
 | ||
|     if ('selectionStart' in input) {
 | ||
|       // Standard-compliant browsers
 | ||
|       return input.selectionStart;
 | ||
|     } else if (document.selection) {
 | ||
|       // IE
 | ||
|       input.fmcus();
 | ||
|       var sel = document.selection.createRange();
 | ||
|       var selLen = document.selection.createRange().text.length;
 | ||
|       sel.moveStart('character', -input.value.length);
 | ||
|       return sel.text.length - selLen;
 | ||
|     }
 | ||
|   }
 | ||
| }(jQuery);
 | ||
| 
 | ||
| // From: http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
 | ||
| 
 | ||
| new function ($) {
 | ||
|   $.fn.setCursorPosition = function (pos) {
 | ||
|     if (this.setSelectionRange) {
 | ||
|       this.setSelectionRange(pos, pos);
 | ||
|     } else if (this.createTextRange) {
 | ||
|       var range = this.createTextRange();
 | ||
|       range.collapse(true);
 | ||
|       if (pos < 0) {
 | ||
|         pos = $(this).val().length + pos;
 | ||
|       }
 | ||
|       range.moveEnd('character', pos);
 | ||
|       range.moveStart('character', pos);
 | ||
|       range.select();
 | ||
|     }
 | ||
|   }
 | ||
| }(jQuery);
 | ||
| 
 | ||
| // Waaaayy overkill here but these ranges were taken from http://www.unicode.org/charts/ and I kinda got carried away.
 | ||
| 
 | ||
| var japaneseTextPattern = /^[\u{3040}-\u{309f}\u{30a0}-\u{30ff}\u{3190}-\u{319f}\u{31f0}-\u{31ff}\u{3400}-\u{4dbf}\u{4e00}-\u{9ffc}\u{f900}-\u{faff}\u{ff00}-\u{ffef}\u{1b000}-\u{1b0ff}\u{1b100}-\u{1b12f}\u{1b130}-\u{1b16f}\u{20000}-\u{2a6dd}\u{2a700}-\u{2b734}\u{2b740}-\u{2b81d}\u{2b820}-\u{2cea1}\u{2ceb0}-\u{2ebe0}\u{2f800}-\u{2fa1f}\u{30000}-\u{3134a}]*$/u;
 | ||
| 
 | ||
| function commaList(items, conjunction) {
 | ||
| 
 | ||
|   if (conjunction == undefined) {
 | ||
|     conjunction = "and";
 | ||
|   }
 | ||
| 
 | ||
|   var result = "";
 | ||
| 
 | ||
|   for (var i = 0; i < items.length; i++) {
 | ||
|     result = result + items[i];
 | ||
| 
 | ||
|     if (i < (items.length - 2)) {
 | ||
|       result += ", ";
 | ||
|     }
 | ||
| 
 | ||
|     if (i == (items.length - 2)) {
 | ||
|       result += " " + conjunction + " ";
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return result;
 | ||
| }
 | ||
| 
 | ||
| function resetLog() {
 | ||
|   log = { "history": [] };
 | ||
| }
 | ||
| 
 | ||
| function kanaForm(words) {
 | ||
| 
 | ||
|   if (words.constructor !== Array) {
 | ||
|     words = [words];
 | ||
|   }
 | ||
| 
 | ||
|   return words.map(function (word) { return word.split(/.\[([^\]]*)\]/).join(""); });
 | ||
| }
 | ||
| 
 | ||
| function kanjiForm(words) {
 | ||
| 
 | ||
|   if (words.constructor !== Array) {
 | ||
|     words = [words];
 | ||
|   }
 | ||
| 
 | ||
|   return words.map(function (word) { return word.split(/(.)\[[^\]]*\]/).join(""); });
 | ||
| }
 | ||
| 
 | ||
| function getVerbForms(entry) {
 | ||
| 
 | ||
|   var result = {
 | ||
|     "kanji": {},
 | ||
|     "hiragana": {},
 | ||
|     "furigana": {}
 | ||
|   };
 | ||
| 
 | ||
|   Object.keys(words[entry].conjugations).forEach(function (key) {
 | ||
|     result["kanji"][key] = kanjiForm(words[entry].conjugations[key].forms);
 | ||
|     result["hiragana"][key] = kanaForm(words[entry].conjugations[key].forms);
 | ||
|     result["furigana"][key] = words[entry].conjugations[key].forms;
 | ||
|   });
 | ||
| 
 | ||
|   return result;
 | ||
| }
 | ||
| 
 | ||
| function wordWithFurigana(words) {
 | ||
| 
 | ||
|   var options = getOptions();
 | ||
| 
 | ||
|   if (words.constructor !== Array) {
 | ||
|     words = [words];
 | ||
|   }
 | ||
| 
 | ||
|   return words.map(function (word) {
 | ||
| 
 | ||
|     var bits = word.split(/(.)\[([^\]]*)\]/);
 | ||
| 
 | ||
|     while (bits.length > 1) {
 | ||
|       if (options["kana"]) {
 | ||
|         bits[0] = bits[0] + bits[2] + bits[3];
 | ||
|       } else if (options["furigana_always"]) {
 | ||
|         bits[0] = bits[0] + "<ruby>" + bits[1] + "<rp>(</rp><rt>" + bits[2] + "</rt><rp>)</rp></ruby>" + bits[3];
 | ||
|       } else {
 | ||
|         bits[0] = bits[0] + "<ruby class='furiganaHover'>" + bits[1] + "<rp>(</rp><rt>" + bits[2] + "</rt><rp>)</rp></ruby>" + bits[3];
 | ||
|       }
 | ||
|       bits.splice(1, 3);
 | ||
|     }
 | ||
| 
 | ||
|     return bits[0];
 | ||
|   });
 | ||
| }
 | ||
| 
 | ||
| function processAnswerKey() {
 | ||
| 
 | ||
|   var el = $('#answer');
 | ||
| 
 | ||
|   var pos = el.getCursorPosition();
 | ||
|   var val = el.val();
 | ||
| 
 | ||
|   var last1 = val.slice(pos - 1, pos);
 | ||
|   var last2 = val.slice(pos - 2, pos);
 | ||
|   var last3 = val.slice(pos - 3, pos);
 | ||
| 
 | ||
|   var replace1 = {
 | ||
|     "a": "あ", "i": "い", "u": "う", "e": "え", "o": "お"
 | ||
|   };
 | ||
| 
 | ||
|   var replace2 = {
 | ||
| 
 | ||
|     "ka": "か", "ki": "き", "ku": "く", "ke": "け", "ko": "こ",
 | ||
|     "sa": "さ", "si": "し", "su": "す", "se": "せ", "so": "そ",
 | ||
|     "ta": "た", "ti": "ち", "tu": "つ", "te": "て", "to": "と",
 | ||
|     "na": "な", "ni": "に", "nu": "ぬ", "ne": "ね", "no": "の",
 | ||
|     "ha": "は", "hi": "ひ", "hu": "ふ", "he": "へ", "ho": "ほ",
 | ||
|     "ma": "ま", "mi": "み", "mu": "む", "me": "め", "mo": "も",
 | ||
|     "ra": "ら", "ri": "り", "ru": "る", "re": "れ", "ro": "ろ",
 | ||
|     "ga": "が", "gi": "ぎ", "gu": "ぐ", "ge": "げ", "go": "ご",
 | ||
|     "za": "ざ", "zi": "じ", "zu": "ず", "ze": "ぜ", "zo": "ぞ",
 | ||
|     "da": "だ", "di": "ぢ", "du": "づ", "de": "で", "do": "ど",
 | ||
|     "ba": "ば", "bi": "び", "bu": "ぶ", "be": "べ", "bo": "ぼ",
 | ||
|     "pa": "ぱ", "pi": "ぴ", "pu": "ぷ", "pe": "ぺ", "po": "ぽ",
 | ||
| 
 | ||
|     "qa": "くぁ", "qi": "くぃ", "qu": "く", "qe": "くぇ", "qo": "くぉ",
 | ||
|     "wa": "わ", "wi": "うぃ", "wu": "う", "we": "うぇ", "wo": "を",
 | ||
|     "ya": "や", "yi": "い", "yu": "ゆ", "ye": "いぇ", "yo": "よ",
 | ||
|     "fa": "ふぁ", "fi": "ふぃ", "fu": "ふ", "fe": "ふぇ", "fo": "ふぉ",
 | ||
|     "ja": "じゃ", "ji": "じ", "ju": "じゅ", "je": "じぇ", "jo": "じょ",
 | ||
|     "la": "ぁ", "li": "ぃ", "lu": "ぅ", "le": "ぇ", "lo": "ぉ",
 | ||
|     "za": "ざ", "zi": "じ", "zu": "ず", "ze": "ぜ", "zo": "ぞ",
 | ||
|     "xa": "ぁ", "xi": "ぃ", "xu": "ぅ", "xe": "ぇ", "xo": "ぉ",
 | ||
|     "ca": "か", "ci": "し", "cu": "く", "ce": "せ", "co": "こ",
 | ||
|     "va": "ヴぁ", "vi": "ヴぃ", "vu": "ヴ", "ve": "ヴぇ", "vo": "ヴぉ",
 | ||
| 
 | ||
|     "lu": "っ",
 | ||
| 
 | ||
|     "nn": "ん", "n'": "ん",
 | ||
| 
 | ||
|     "nb": "んb", "nc": "んc", "nd": "んd", "nf": "んf", "ng": "んg",
 | ||
|     "nh": "んh", "nj": "んj", "nk": "んk", "nl": "んl", "nm": "んm",
 | ||
|     "np": "んp", "nq": "んq", "nr": "んr", "ns": "んs", "nt": "んt",
 | ||
|     "nv": "んv", "nw": "んw", "nx": "んx", "nz": "んz",
 | ||
| 
 | ||
|     "aa": "っa", "bb": "っb", "cc": "っc", "dd": "っd", "ee": "っe",
 | ||
|     "ff": "っf", "gg": "っg", "hh": "っh", "ii": "っi", "jj": "っj",
 | ||
|     "kk": "っk", "ll": "っl", "mm": "っm", "oo": "っo", "pp": "っp",
 | ||
|     "qq": "っq", "rr": "っr", "ss": "っs", "tt": "っt", "uu": "っu",
 | ||
|     "vv": "っv", "ww": "っw", "xx": "っx", "yy": "っy", "zz": "っz",
 | ||
|   };
 | ||
| 
 | ||
|   var replace3 = {
 | ||
| 
 | ||
|     "kya": "きゃ", "kyi": "きぃ", "kyu": "きゅ", "kye": "きぇ", "kyo": "きょ",
 | ||
|     "sha": "しゃ", "shi": "し", "shu": "しゅ", "she": "しぇ", "sho": "しょ",
 | ||
|     "cha": "ちゃ", "chi": "ち", "chu": "ちゅ", "che": "ちぇ", "cho": "ちょ",
 | ||
|     "nya": "にゃ", "nyi": "にぃ", "nyu": "にゅ", "nye": "にぇ", "nyo": "にょ",
 | ||
|     "hya": "ひゃ", "hyi": "ひぃ", "hyu": "ひゅ", "hye": "ひぇ", "hyo": "ひょ",
 | ||
|     "mya": "みゃ", "myi": "みぃ", "myu": "みゅ", "mye": "みぇ", "myo": "みょ",
 | ||
|     "rya": "りゃ", "ryi": "りぃ", "ryu": "りゅ", "rye": "りぇ", "ryo": "りょ",
 | ||
|     "gya": "ぎゃ", "gyi": "ぎぃ", "gyu": "ぎゅ", "gye": "ぎぇ", "gyo": "ぎょ",
 | ||
|     "zya": "じゃ", "zyi": "じぃ", "zyu": "じゅ", "zye": "じぇ", "zyo": "じょ",
 | ||
|     "dya": "ぢゃ", "dyi": "ぢぃ", "dyu": "ぢゅ", "dye": "ぢぇ", "dyo": "ぢょ",
 | ||
|     "bya": "びゃ", "byi": "びぃ", "byu": "びゅ", "bye": "びぇ", "byo": "びょ",
 | ||
|     "pya": "ぴゃ", "pyi": "ぴぃ", "pyu": "ぴゅ", "pye": "ぴぇ", "pyo": "ぴょ",
 | ||
| 
 | ||
|     "shi": "し",
 | ||
|     "tsu": "つ",
 | ||
|   };
 | ||
| 
 | ||
|   if (replace3[last3]) {
 | ||
|     val = val.slice(0, pos - 3) + replace3[last3] + val.slice(pos, -1);
 | ||
|     el.val(val);
 | ||
|     el.setCursorPosition(pos - 3 + replace3[last3].length);
 | ||
|   } else if (replace2[last2]) {
 | ||
|     val = val.slice(0, pos - 2) + replace2[last2] + val.slice(pos, -1);
 | ||
|     el.val(val);
 | ||
|     el.setCursorPosition(pos - 2 + replace2[last2].length);
 | ||
|   } else if (replace1[last1]) {
 | ||
|     val = val.slice(0, pos - 1) + replace1[last1] + val.slice(pos, -1);
 | ||
|     el.val(val);
 | ||
|     el.setCursorPosition(pos - 1 + replace1[last1].length);
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function validQuestion(entry, forms, transformation, options) {
 | ||
| 
 | ||
|   var valid = true;
 | ||
| 
 | ||
|   transformation.tags.forEach(function (type) {
 | ||
|     if (options[type] == false) {
 | ||
|       valid = false;
 | ||
|     }
 | ||
|   });
 | ||
| 
 | ||
|   if (options[words[entry].group] == false) {
 | ||
|     valid = false;
 | ||
|   }
 | ||
| 
 | ||
|   if (!forms["furigana"][transformation.from])
 | ||
|     valid = false;
 | ||
| 
 | ||
|   if (!forms["furigana"][transformation.to])
 | ||
|     valid = false;
 | ||
| 
 | ||
|   if (valid) {
 | ||
| 
 | ||
|     if (options.questionFocus != "none") {
 | ||
| 
 | ||
|       if (options.questionFocus == 'tetakei') {
 | ||
|         // console.log("tetakei", words[entry].conjugations[transformation.from].tetakei, words[entry].conjugations[transformation.to].tetakei)
 | ||
|         if (words[entry].conjugations[transformation.from].tetakei == words[entry].conjugations[transformation.to].tetakei) {
 | ||
|           valid = false;
 | ||
|         }
 | ||
|       } else if (transformation.type != options.questionFocus) {
 | ||
|         valid = false;
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return valid;
 | ||
| }
 | ||
| 
 | ||
| function generateQuestion() {
 | ||
| 
 | ||
|   var questionText = {
 | ||
|     "affirmative": "<span class='first'>make</span> the following <span class='emphasis'>affirmative</span>",
 | ||
|     "negative": "<span class='first'>make</span> the following <span class='emphasis'>negative</span>",
 | ||
|     "present": "<span class='first'>convert</span> the following to the <span class='emphasis'>present tense</span>",
 | ||
|     "past": "<span class='first'>convert</span> the following to the <span class='emphasis'>past tense</span>",
 | ||
|     "plain": "<span class='first'>make</span> the following <span class='emphasis'>informal</span>",
 | ||
|     "polite": "<span class='first'>make</span> the following <span class='emphasis'>polite</span>",
 | ||
|     "て": "<span class='emphasis first'>add</span> the <span class='emphasis'>て pattern</span> to the following",
 | ||
|     "non-て": "<span class='emphasis first'>remove</span> the <span class='emphasis'>て pattern</span> from the following",
 | ||
|     "potential": "<span class='first'>make</span> the following <span class='emphasis'>potential</span>",
 | ||
|     "non-potential": "<span class='first'>make</span> the following <span class='emphasis'>non-potential</span>",
 | ||
|     "imperative": "<span class='first'>make</span> the following <span class='emphasis'>imperative</span>",
 | ||
|     "non-imperative": "<span class='first'>make</span> the following <span class='emphasis'>non-imperative</span>",
 | ||
|     "causative": "<span class='first'>make</span> the following <span class='emphasis'>causative</span>",
 | ||
|     "non-causative": "<span class='first'>make</span> the following <span class='emphasis'>non-causative</span>",
 | ||
|     "passive": "<span class='first'>make</span> the following <span class='emphasis'>passive</span>",
 | ||
|     "active": "<span class='first'>make</span> the following <span class='emphasis'>active</span>",
 | ||
|     "progressive": "<span class='first'>make</span> the following <span class='emphasis'>progressive</span>",
 | ||
|     "non-progressive": "<span class='first'>make</span> the following <span class='emphasis'>non-progressive</span>",
 | ||
|     "'desire'": "<span class='first'>convert</span> the following to the <span class='emphasis'>'desire'</span> form",
 | ||
|     "'non-desire'": "<span class='first'>convert</span> the following to the <span class='emphasis'>'non-desire'</span> form",
 | ||
|     "volitional": "<span class='first'>make</span> the following <span class='emphasis'>volitional</span>",
 | ||
|     "non-volitional": "<span class='first'>make</span> the following <span class='emphasis'>non-volitional</span>"
 | ||
|   };
 | ||
| 
 | ||
|   var entry;
 | ||
|   var to_form;
 | ||
|   var from_form;
 | ||
|   var forms;
 | ||
|   var options = getOptions();
 | ||
| 
 | ||
|   var count = 0;
 | ||
| 
 | ||
|   while (true) {
 | ||
| 
 | ||
|     if (count++ == 10000) {
 | ||
|       showSplash();
 | ||
|       return;
 | ||
|     }
 | ||
| 
 | ||
|     entry = Object.keys(words).randomElement();
 | ||
|     transformation = transformations.randomElement();
 | ||
| 
 | ||
|     from_form = transformation.from;
 | ||
|     to_form = transformation.to;
 | ||
| 
 | ||
|     forms = getVerbForms(entry);
 | ||
| 
 | ||
|     var valid = validQuestion(entry, forms, transformation, getOptions());
 | ||
| 
 | ||
|     // Modify the chance of trick questions so that they appear on average 25%
 | ||
|     // of the time. When trick questions are active then 50% of the
 | ||
|     // transformation structure are trick questions and so a 33% filter here
 | ||
|     // will achieve the 25% because this test is only performed when a trick
 | ||
|     // question has been selected.
 | ||
| 
 | ||
|     if (transformation.tags.indexOf('trick') != -1) {
 | ||
|       if (Math.random() > 0.333) {
 | ||
|         valid = false;
 | ||
|       }
 | ||
|     }
 | ||
| 
 | ||
|     if (valid) {
 | ||
|       break;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   var kanjiForms = forms["kanji"];
 | ||
|   var kanaForms = forms["hiragana"];
 | ||
|   var furiganaForms = forms["furigana"];
 | ||
| 
 | ||
|   var givenWord;
 | ||
| 
 | ||
|   if (options["kana"]) {
 | ||
|     givenWord = kanaForms[from_form].randomElement();
 | ||
|   } else {
 | ||
|     givenWord = wordWithFurigana(furiganaForms[from_form]).randomElement();
 | ||
|   }
 | ||
| 
 | ||
|   var thisQuestionText = questionText[transformation.phrase];
 | ||
| 
 | ||
|   // thisQuestionText = thisQuestionText[0].toUpperCase() + thisQuestionText.substring(1);
 | ||
| 
 | ||
|   var questionFirstHalf = thisQuestionText;
 | ||
|   var questionSecondHalf = givenWord;
 | ||
| 
 | ||
|   var question = questionFirstHalf.replace("the following", questionSecondHalf);
 | ||
| 
 | ||
|   var answer = kanjiForms[to_form];
 | ||
|   var answer2 = kanaForms[to_form];
 | ||
|   var answerWithFurigana = wordWithFurigana(furiganaForms[to_form]);
 | ||
| 
 | ||
|   if (options["kana"]) {
 | ||
|     answer = answer2;
 | ||
|     answerWithFurigana = kanaForms[to_form];
 | ||
|   }
 | ||
| 
 | ||
|   $('#questionFirstHalf').html(questionFirstHalf);
 | ||
|   $('#questionSecondHalf').html(questionSecondHalf);
 | ||
| 
 | ||
|   window.questionData = {
 | ||
|     entry: entry,
 | ||
|     transformation: transformation,
 | ||
|     question: question,
 | ||
|     answer: answer,
 | ||
|     answer2: answer2,
 | ||
|     answerWithFurigana: answerWithFurigana,
 | ||
|     givenWord: givenWord,
 | ||
|   };
 | ||
| 
 | ||
|   // Construct the explanation page.
 | ||
| 
 | ||
|   var data = window.questionData;
 | ||
| 
 | ||
|   var groupLabels = {
 | ||
|     "godan": "godan verb",
 | ||
|     "ichidan": "ichidan verb",
 | ||
|     "iku": "godan verb",
 | ||
|     "suru": "suru verb",
 | ||
|     "kuru": "special verb",
 | ||
|     "i-adjective": "い-adjective",
 | ||
|     "ii": "い-adjective",
 | ||
|     "na-adjective": "な-adjective",
 | ||
|   };
 | ||
| 
 | ||
|   var dictionary = words[data.entry].conjugations["dictionary"].forms;
 | ||
| 
 | ||
|   if (words[data.entry].group == "na-adjective") {
 | ||
|     dictionary = dictionary.replace(/だ$/, '')
 | ||
|   }
 | ||
| 
 | ||
|   if (!options["kana"]) {
 | ||
|     dictionary = wordWithFurigana(dictionary);
 | ||
|   } else {
 | ||
|     dictionary = kanaForm(dictionary);
 | ||
|   }
 | ||
| 
 | ||
|   $('#explain-given').html(givenWord);
 | ||
|   $('#explain-given-tags').html(data.transformation.from_tags.map(function (tag) { return "<span class='tag'>" + tag + "</span>"; }).join(" "));
 | ||
|   $('.explain-given-dictionary').html(dictionary);
 | ||
|   $('#explain-group').html(groupLabels[words[data.entry].group]);
 | ||
|   $('.explain-transform').html(data.transformation.phrase);
 | ||
|   $('.explain-answer-tags').html(data.transformation.to_tags.map(function (tag) { return "<span class='tag'>" + tag + "</span>"; }).join(" "));
 | ||
|   $('.explain-answer-tags2').html(data.transformation.to_tags.join(" "));
 | ||
|   $('.explain-answer').html(commaList(questionData.answerWithFurigana, "or"));
 | ||
| 
 | ||
|   $('.explain-answer-as-list').empty();
 | ||
| 
 | ||
|   questionData.answerWithFurigana.forEach(function (answer) {
 | ||
|     $('.explain-answer-as-list').append("<li>" + answer);
 | ||
|   });
 | ||
| 
 | ||
|   if (window.questionData.transformation.tags.indexOf("trick") != -1) {
 | ||
|     $('.explain-trick').show();
 | ||
|     $('.explain-no-trick').hide();
 | ||
|   } else {
 | ||
|     $('.explain-trick').hide();
 | ||
|     $('.explain-no-trick').show();
 | ||
|   }
 | ||
| 
 | ||
|   if (data.transformation.to == "dictionary") {
 | ||
|     $('.explain-hide-end').hide();
 | ||
|   } else {
 | ||
|     $('.explain-hide-end').show();
 | ||
|   }
 | ||
| 
 | ||
|   if (data.answer.length == 1) {
 | ||
|     $('.explain-answer-single').show();
 | ||
|     $('.explain-answer-multiple').hide();
 | ||
|   } else {
 | ||
|     $('.explain-answer-single').hide();
 | ||
|     $('.explain-answer-multiple').show();
 | ||
|   }
 | ||
| 
 | ||
|   $('#next').prop('disabled', true);
 | ||
|   $('#response').html("");
 | ||
|   $('#message').html("");
 | ||
| 
 | ||
|   $('#proceed').hide();
 | ||
|   $('#explanation').hide();
 | ||
|   $('#inputArea').show();
 | ||
|   $('#answer').focus();
 | ||
| 
 | ||
|   $('#answer').on('input', processAnswerKey);
 | ||
| }
 | ||
| 
 | ||
| function processAnswer() {
 | ||
| 
 | ||
|   var questionData = window.questionData;
 | ||
|   var response = $('#answer').val().trim();
 | ||
| 
 | ||
|   var shake = false;
 | ||
| 
 | ||
|   if (response == "")
 | ||
|     shake = true;
 | ||
| 
 | ||
|   if (!response.match(japaneseTextPattern))
 | ||
|     shake = true;
 | ||
| 
 | ||
|   if (shake) {
 | ||
|     shakeInputArea();
 | ||
|     return;
 | ||
|   }
 | ||
| 
 | ||
|   var correct = ((questionData.answer.indexOf(response) != -1) || (questionData.answer2.indexOf(response) != -1));
 | ||
| 
 | ||
|   var klass = correct ? "correct" : "incorrect";
 | ||
| 
 | ||
|   log.history.push({
 | ||
|     "question": questionData.question,
 | ||
|     "response": response,
 | ||
|     "answer": questionData.answerWithFurigana,
 | ||
|     "kana": questionData.answer2,
 | ||
|     "correct": correct
 | ||
|   });
 | ||
| 
 | ||
|   $('#answer').val("");
 | ||
|   $('#responseButton').prop('class', klass).text(response);
 | ||
|   $('#next').prop('disabled', false);
 | ||
| 
 | ||
|   if (correct) {
 | ||
|     $('#message').html("");
 | ||
|   } else {
 | ||
|     $('#message').show();
 | ||
|     $('#message').html("<div>The correct answer was " + commaList(questionData.answerWithFurigana, "or") + " <button class='btn btn-primary mb-2 mr-sm-2' onclick='explain()'>Explain</button></div>");
 | ||
|   }
 | ||
| 
 | ||
|   $('#inputArea').hide();
 | ||
|   $('#proceed').show();
 | ||
|   $('#explanation').hide();
 | ||
|   $('#proceed button').focus();
 | ||
| 
 | ||
|   updateHistoryView(log);
 | ||
| }
 | ||
| 
 | ||
| function shakeInputArea() {
 | ||
| 
 | ||
|   var inputArea = $('#inputArea');
 | ||
|   var shakeClass = "shake";
 | ||
| 
 | ||
|   inputArea.addClass(shakeClass);
 | ||
| 
 | ||
|   setTimeout(function () {
 | ||
|     inputArea.removeClass(shakeClass)
 | ||
|   }, 1000);
 | ||
| }
 | ||
| 
 | ||
| function updateHistoryView(log) {
 | ||
| 
 | ||
|   var review = $('<div>');
 | ||
| 
 | ||
|   var total = 0;
 | ||
|   var correct = 0;
 | ||
| 
 | ||
|   log.history.forEach(function (entry, index) {
 | ||
| 
 | ||
|     total++;
 | ||
| 
 | ||
|     if (entry.correct) {
 | ||
|       correct++;
 | ||
|     }
 | ||
| 
 | ||
|     var tr = $('<div class="row mt-4">');
 | ||
| 
 | ||
|     var td1 = $('<div class="col-md-6 mb-2">');
 | ||
|     var td2 = $('<div class="col-md-3 ml-4 ml-md-0">');
 | ||
| 
 | ||
|     td1.html((index + 1) + ". " + entry.question + ".");
 | ||
| 
 | ||
|     var responseDiv = $('<div>');
 | ||
|     responseDiv.text(entry.response);
 | ||
| 
 | ||
|     if (entry.correct) {
 | ||
|       responseDiv.append("<span class='answer-correct'> 〇</span>");
 | ||
|     } else {
 | ||
|       responseDiv.append("<span class='answer-wrong'> ×</span>");
 | ||
|     }
 | ||
| 
 | ||
|     td2.append(responseDiv);
 | ||
| 
 | ||
|     if (!entry.correct) {
 | ||
| 
 | ||
|       var correctDiv = $('<div>');
 | ||
| 
 | ||
|       correctDiv.html(commaList(entry.answer, "or"));
 | ||
|       correctDiv.append("<span class='answer-correct'> 〇</span>");
 | ||
| 
 | ||
|       td2.append(correctDiv);
 | ||
|     }
 | ||
| 
 | ||
|     tr.append(td1);
 | ||
|     tr.append(td2);
 | ||
| 
 | ||
|     review.append(tr);
 | ||
|   });
 | ||
| 
 | ||
|   $('#history').empty().append(review);
 | ||
| 
 | ||
|   var resultString;
 | ||
| 
 | ||
|   if (correct == total) {
 | ||
|     resultString = "All correct!";
 | ||
|   } else if (correct == 0) {
 | ||
|     resultString = "All incorrect!";
 | ||
|   } else {
 | ||
|     resultString = correct + " of " + total + " correct";
 | ||
|   }
 | ||
| 
 | ||
|   $('#scoreSectionTitleNarrow').text(resultString);
 | ||
|   $('#scoreSectionTitleWide').text(resultString);
 | ||
| }
 | ||
| 
 | ||
| function proceed() {
 | ||
|   if (log.history.length == $('#numQuestions').val()) {
 | ||
|     endQuiz();
 | ||
|   } else {
 | ||
|     generateQuestion();
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| function showSplash() {
 | ||
|   $('#splash').show();
 | ||
|   $('#quizSection').hide();
 | ||
|   $('#scoreSection').hide();
 | ||
| 
 | ||
|   $('#go').focus();
 | ||
| }
 | ||
| 
 | ||
| function startQuiz() {
 | ||
|   $('#splash').hide();
 | ||
|   $('#quizSection').show();
 | ||
|   $('#scoreSection').hide();
 | ||
| 
 | ||
|   var options = getOptions();
 | ||
| 
 | ||
|   if (options.furigana_always) {
 | ||
|     $('body').addClass("furiganaAlways");
 | ||
|   } else {
 | ||
|     $('body').removeClass("furiganaAlways");
 | ||
|   }
 | ||
| 
 | ||
|   resetLog();
 | ||
|   generateQuestion();
 | ||
| }
 | ||
| 
 | ||
| function endQuiz() {
 | ||
|   $('#splash').hide();
 | ||
|   $('#quizSection').hide();
 | ||
|   $('#scoreSection').show();
 | ||
| 
 | ||
|   $('#backToStart').focus();
 | ||
| }
 | ||
| 
 | ||
| function arrayDifference(a, b) {
 | ||
|   // From http://stackoverflow.com/a/1723220
 | ||
|   return a.filter(function (x) { return b.indexOf(x) < 0 });
 | ||
| }
 | ||
| 
 | ||
| function arrayUnique(arr) {
 | ||
|   return arr.filter(function (value, index, self) {
 | ||
|     return self.indexOf(value) === index;
 | ||
|   });
 | ||
| }
 | ||
| 
 | ||
| function calculateTransitions() {
 | ||
| 
 | ||
|   function getTags(str) {
 | ||
| 
 | ||
|     var tags = str.split(" ");
 | ||
| 
 | ||
|     if ((tags.length == 1) && (tags[0] == "plain")) {
 | ||
|       tags = [];
 | ||
|     }
 | ||
| 
 | ||
|     return tags;
 | ||
|   }
 | ||
| 
 | ||
|   function calculateTags(tags) {
 | ||
| 
 | ||
|     tags = tags.split(" ");
 | ||
| 
 | ||
|     if (tags.indexOf("polite") == -1) {
 | ||
|       tags.splice(0, 0, "plain");
 | ||
|     }
 | ||
| 
 | ||
|     if (tags.indexOf("dictionary") != -1) {
 | ||
|       tags.splice(tags.indexOf("dictionary"), 1);
 | ||
|     }
 | ||
| 
 | ||
|     return tags;
 | ||
|   }
 | ||
| 
 | ||
|   var allTags = {};
 | ||
| 
 | ||
|   Object.keys(words).forEach(function (word) {
 | ||
| 
 | ||
|     Object.keys(words[word].conjugations).forEach(function (conjugation) {
 | ||
| 
 | ||
|       if (conjugation == "dictionary") {
 | ||
|         conjugation = "";
 | ||
|       }
 | ||
| 
 | ||
|       allTags[conjugation] = conjugation.split(" ");
 | ||
|     });
 | ||
|   });
 | ||
| 
 | ||
|   Object.keys(allTags).forEach(function (srcTag) {
 | ||
| 
 | ||
|     if (srcTag != "") {
 | ||
| 
 | ||
|       for (var i = 0; i < allTags[srcTag].length; i++) {
 | ||
| 
 | ||
|         var tagWithDrop = allTags[srcTag].slice();
 | ||
| 
 | ||
|         tagWithDrop.splice(i, 1);
 | ||
| 
 | ||
|         var dstTag = tagWithDrop.join(" ");
 | ||
| 
 | ||
|         if (allTags[dstTag]) {
 | ||
| 
 | ||
|           if (srcTag == "") {
 | ||
|             srcTag = "dictionary";
 | ||
|           }
 | ||
| 
 | ||
|           if (dstTag == "") {
 | ||
|             dstTag = "dictionary";
 | ||
|           }
 | ||
| 
 | ||
|           transformations.push({ from: srcTag, to: dstTag });
 | ||
|           transformations.push({ from: dstTag, to: srcTag });
 | ||
|         }
 | ||
|       }
 | ||
|     }
 | ||
|   });
 | ||
| 
 | ||
|   transformations.forEach(function (transformation) {
 | ||
| 
 | ||
|     var from = getTags(transformation.from);
 | ||
|     var to = getTags(transformation.to);
 | ||
| 
 | ||
|     var from_extra = {
 | ||
|       "negative": "affirmative",
 | ||
|       "past": "present",
 | ||
|       "polite": "plain",
 | ||
|       "te-form": "non-て",
 | ||
|       "potential": "non-potential",
 | ||
|       "imperative": "non-imperative",
 | ||
|       "causative": "non-causative",
 | ||
|       "passive": "active",
 | ||
|       "progressive": "non-progressive",
 | ||
|       "desire": "'non-desire'",
 | ||
|       "volitional": "non-volitional",
 | ||
|     };
 | ||
| 
 | ||
|     var to_extra = {
 | ||
|       "negative": "negative",
 | ||
|       "past": "past",
 | ||
|       "polite": "polite",
 | ||
|       "te-form": "て",
 | ||
|       "potential": "potential",
 | ||
|       "imperative": "imperative",
 | ||
|       "causative": "causative",
 | ||
|       "passive": "passive",
 | ||
|       "progressive": "progressive",
 | ||
|       "desire": "'desire'",
 | ||
|       "volitional": "volitional",
 | ||
|     };
 | ||
| 
 | ||
|     var phrase;
 | ||
| 
 | ||
|     phrase = phrase || from_extra[arrayDifference(from, to)[0]];
 | ||
|     phrase = phrase || to_extra[arrayDifference(to, from)[0]];
 | ||
| 
 | ||
|     transformation.phrase = phrase;
 | ||
| 
 | ||
|     transformation.from_tags = calculateTags(transformation.from);
 | ||
|     transformation.to_tags = calculateTags(transformation.to);
 | ||
|     transformation.tags = arrayUnique(calculateTags(transformation.from).concat(calculateTags(transformation.to)));
 | ||
| 
 | ||
|     var diffFromTo = arrayDifference(transformation.from_tags, transformation.to_tags);
 | ||
| 
 | ||
|     if (diffFromTo.length > 0) {
 | ||
|       type = diffFromTo[0];
 | ||
|     } else {
 | ||
|       type = arrayDifference(transformation.to_tags, transformation.from_tags)[0];
 | ||
|     }
 | ||
| 
 | ||
|     if ((type == "plain") || (type == "polite")) {
 | ||
|       type = "politeness";
 | ||
|     }
 | ||
| 
 | ||
|     transformation.type = type;
 | ||
|   });
 | ||
| 
 | ||
|   // Add trick forms
 | ||
| 
 | ||
|   var trick_forms = [];
 | ||
| 
 | ||
|   transformations.forEach(function (transformation) {
 | ||
|     trick_forms.push({
 | ||
|       from: transformation.to,
 | ||
|       to: transformation.to,
 | ||
|       type: transformation.type,
 | ||
|       phrase: transformation.phrase,
 | ||
|       from_tags: transformation.to_tags,
 | ||
|       to_tags: transformation.to_tags,
 | ||
|       tags: transformation.tags.concat(["trick"])
 | ||
|     });
 | ||
|   });
 | ||
| 
 | ||
|   transformations = transformations.concat(trick_forms);
 | ||
| }
 | ||
| 
 | ||
| function updateOptionSummary() {
 | ||
| 
 | ||
|   // Calculate how many questions will apply
 | ||
| 
 | ||
|   var options = getOptions();
 | ||
|   var applicable = 0;
 | ||
| 
 | ||
|   Object.keys(words).forEach(function (word) {
 | ||
| 
 | ||
|     var forms = getVerbForms(word);
 | ||
| 
 | ||
|     transformations.forEach(function (transformation) {
 | ||
| 
 | ||
|       if (validQuestion(word, forms, transformation, options)) {
 | ||
|         applicable++;
 | ||
|       }
 | ||
|     });
 | ||
|   });
 | ||
| 
 | ||
|   $("#questionCount").text(applicable);
 | ||
| }
 | ||
| 
 | ||
| function explain() {
 | ||
|   $('#explanation').show();
 | ||
|   $('#message').hide();
 | ||
|   $('#explain-proceed-button').focus();
 | ||
| }
 | ||
| 
 | ||
| function getOptions() {
 | ||
| 
 | ||
|   var options = ["plain", "polite", "negative", "past", "te-form",
 | ||
|     "progressive", "potential", "imperative", "passive", "causative",
 | ||
|     "godan", "ichidan", "iku", "kuru", "suru", "i-adjective", "na-adjective",
 | ||
|     "ii", "desire", "volitional", "trick", "kana", "furigana_always"];
 | ||
| 
 | ||
|   var selects = ["questionFocus"];
 | ||
| 
 | ||
|   var result = {};
 | ||
| 
 | ||
|   options.forEach(function (option) {
 | ||
|     result[option] = $('#' + option).is(':checked') != false;
 | ||
|   });
 | ||
| 
 | ||
|   selects.forEach(function (select) {
 | ||
|     result[select] = $('#' + select).val();
 | ||
|   });
 | ||
| 
 | ||
|   return result;
 | ||
| }
 | ||
| 
 | ||
| $('window').ready(function () {
 | ||
| 
 | ||
|   calculateAllConjugations();
 | ||
|   calculateTransitions();
 | ||
| 
 | ||
|   $('#go').click(startQuiz);
 | ||
|   $('#backToStart').click(showSplash);
 | ||
| 
 | ||
|   $('div.options input').click(updateOptionSummary);
 | ||
|   $('select#questionFocus').on('change', updateOptionSummary);
 | ||
|   $('input#trick').click(updateOptionSummary);
 | ||
|   $('input#focus_mode').click(updateOptionSummary);
 | ||
| 
 | ||
|   updateOptionSummary();
 | ||
| 
 | ||
|   showSplash();
 | ||
| });
 | 
