spanish-quizzer

An app to quiz you on Spanish vocabulary and verb conjugations
git clone https://git.ashermorgan.net/spanish-quizzer/
Log | Files | Refs | README

commit 2eb59833240b87418b355c5b4f787eda5f6172af
parent db6277fae72afd7da88c16637f31596030b35d73
Author: AsherMorgan <59518073+AsherMorgan@users.noreply.github.com>
Date:   Sun, 21 Feb 2021 16:45:00 -0800

Merge pull request #26 from AsherMorgan/spa

Refactor into Single Page App
Diffstat:
MREADME.md | 2+-
Mcss/app.css | 47+++--------------------------------------------
Dcss/filterInput.css | 69---------------------------------------------------------------------
Acss/filtersPage.css | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcss/global.css | 5++---
Mcss/quizzer.css | 30+++++++++++++++++++-----------
Mcss/reference.css | 14+++++++-------
Dcss/settingsInput.css | 15---------------
Aimages/x.svg | 2++
Mindex.html | 68+++++++++++++++++---------------------------------------------------
Mjs/app.js | 255+++++++++++++++++++++++++++++++++----------------------------------------------
Djs/filterInput.js | 301-------------------------------------------------------------------------------
Ajs/filtersPage.js | 511+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/global.js | 16----------------
Mjs/quizzer.js | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mjs/reference.js | 127+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Djs/settingsInput.js | 82-------------------------------------------------------------------------------
Dreference.html | 58----------------------------------------------------------
Mtests/index.html | 8++------
Dtests/test.app.js | 81-------------------------------------------------------------------------------
Dtests/test.filterInput.js | 378-------------------------------------------------------------------------------
Atests/test.filtersPage.js | 488+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test.global.js | 17-----------------
Mtests/test.quizzer.js | 39+++++++++++++++++++++------------------
Dtests/test.settingsInput.js | 60------------------------------------------------------------
25 files changed, 1482 insertions(+), 1447 deletions(-)

diff --git a/README.md b/README.md @@ -6,7 +6,7 @@ Try it out [here](https://ashermorgan.github.io/Spanish-Quizzer/). - Supports practice for all four language skills: reading, listening, writing, and speaking. - Lots of filters for both vocabulary and verb conjugations. - Easily type special characters (``` a` ``` → `á`, `n~` → `ñ`, `u~` → `ü`) -- Spanish reference tables (available [here](https://ashermorgan.github.io/Spanish-Quizzer/reference.html)) +- Spanish reference tables ## Vocabulary Filters The following filters can be used on vocabulary: diff --git a/css/app.css b/css/app.css @@ -1,5 +1,5 @@ /* Header */ -#home > h1 { +.home main h1 { font-size: 20px; font-weight: bold; margin-top: 15px; @@ -7,7 +7,7 @@ } /* Vocab & Conjugation buttons*/ -#home button { +.home main button { width: 150px; height: 50px; line-height: 50px; @@ -16,47 +16,6 @@ } /* Link to Reference Tables */ -#home a { +.home main a { margin-top: 10px; } - -/* Filters and Settings */ -#settings { - display: flex; - flex-direction: column; - align-items: center; -} -#settings[hidden] { - display: none; -} - -/* Start/Resume Buttons */ -.settingButtons { - margin-top: 20px; - margin-bottom: 10px; -} -.settingButtons button { - width: 100px; - height: 40px; - font-size: 18px; - margin: 0px 5px; -} -@media only screen and (max-width: 510px) { - /* Expand Start/Resume buttons */ - .settingButtons { - width: calc(100% - 10px); - } - .settingButtons button { - width: calc(50% - 15px); - height: 50px; - } -} - -/* Congrats message */ -#congrats p { - margin: 20px; -} -#congrats button { - width: 100px; - height: 35px; -} diff --git a/css/filterInput.css b/css/filterInput.css @@ -1,69 +0,0 @@ -/* FilterInput */ -.filtersInput h1 { - font-size: 16px; - font-weight: normal; - margin-bottom: 10px; -} -.filtersInput h2 { - font-size: 20px; - margin-bottom: 5px; -} -.filtersInput div { - margin-left: 10px; - margin-right: 10px; - margin-top: 10px; -} -.filtersInput div h2 button { - padding: 3px; -} - -/* Filter */ -.filter { - width: max-content; - - background-color: var(--background-color); - border: 1px solid var(--border-color); - box-shadow: 0 0 15px var(--border-color); - - margin: 5px auto; - padding: 5px; - border-radius: 5px; -} -.filter button { - border: none; - background-color: var(--background-color); - color: var(--foreground-color); - cursor: pointer; -} - -/* Conjugation settings on mobile devices */ -@media only screen and (max-width: 510px) { - .verbSettings { - width: calc(100% - 20px); - } - .verbSettings .filter { - width: 100%; - margin: 10px auto; - } - .verbSettings .filter select, .verbSettings .filter button { - width: 100%; - height: 30px; - margin: 3px 0px; - } -} - -/* Vocab settings on mobile devices */ -@media only screen and (max-width: 350px) { - .vocabSettings { - width: calc(100% - 20px); - } - .vocabSettings .filter { - width: 100%; - margin: 10px auto; - } - .vocabSettings .filter select, .vocabSettings .filter button { - width: 100%; - height: 30px; - margin: 3px 0px; - } -} diff --git a/css/filtersPage.css b/css/filtersPage.css @@ -0,0 +1,119 @@ +/******** filter-input ********/ +.filtersInput h1 { + font-size: 16px; + font-weight: normal; + margin-bottom: 10px; +} +.filtersInput h2 { + font-size: 20px; + margin-bottom: 5px; +} +.filtersInput div { + margin-left: 10px; + margin-right: 10px; + margin-top: 10px; +} +.filtersInput div h2 button { + padding: 3px; +} + +/* Filter */ +.filter { + width: max-content; + + background-color: var(--background-color); + border: 1px solid var(--border-color); + box-shadow: 0 0 15px var(--border-color); + + margin: 5px auto; + padding: 5px; + border-radius: 5px; +} +.filter button { + border: none; + background-color: var(--background-color); + color: var(--foreground-color); + cursor: pointer; +} + +/* Conjugation settings on mobile devices */ +@media only screen and (max-width: 510px) { + .verbSettings { + width: calc(100% - 20px); + } + .verbSettings .filter { + width: 100%; + margin: 10px auto; + } + .verbSettings .filter select, .verbSettings .filter button { + width: 100%; + height: 30px; + margin: 3px 0px; + } +} + +/* Vocab settings on mobile devices */ +@media only screen and (max-width: 350px) { + .vocabSettings { + width: calc(100% - 20px); + } + .vocabSettings .filter { + width: 100%; + margin: 10px auto; + } + .vocabSettings .filter select, .vocabSettings .filter button { + width: 100%; + height: 30px; + margin: 3px 0px; + } +} + + + +/******** settings-input ********/ +.settingsInput h2 { + font-size: 20px; + margin-bottom: 5px; +} +.settingsInput { + margin-left: 5px; + margin-right: 5px; + margin-top: 10px; +} +.settingsInput div { + text-align: left; +} +.settingsInput input[type=number] { + width: 50px; +} + + + +/******** filters-page ********/ +.filtersPage main { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Start/Resume Buttons */ +.settingButtons { + margin-top: 20px; + margin-bottom: 10px; +} +.settingButtons button { + width: 100px; + height: 40px; + font-size: 18px; + margin: 0px 5px; +} +@media only screen and (max-width: 510px) { + /* Expand Start/Resume buttons */ + .settingButtons { + width: calc(100% - 10px); + } + .settingButtons button { + width: calc(50% - 15px); + height: 50px; + } +} diff --git a/css/global.css b/css/global.css @@ -30,7 +30,7 @@ body { touch-action: manipulation; } -#app { +#app>div { width: 100%; height: 100%; @@ -92,8 +92,7 @@ header { padding-top: 15px; padding-bottom: 15px; } - -#backArrow { +header img { position: absolute; left: 10px; height: 30px; diff --git a/css/quizzer.css b/css/quizzer.css @@ -1,6 +1,6 @@ /******** Quizzer styles ********/ /* Container */ -#quizzer { +.quizzer { margin: 10px; padding: 10px; width: fit-content; @@ -11,7 +11,7 @@ } /* Progress */ -#quizzerProgress { +.quizzerProgress { font-size: 18px; font-weight: bold; margin: 0px; @@ -19,20 +19,20 @@ } /* Prompt */ -#quizzer section { +.quizzer section { text-align: left; margin-bottom: 15px; } -#quizzerPromptType, #quizzerInputType { +.quizzerPromptType, .quizzerInputType { font-weight: bold; } -#quizzerPrompt { +.quizzerPrompt { cursor: pointer; font-size: 15px; } /* Responce*/ -#quizzerInput { +.quizzerInput { margin-top: 5px; height: 30px; padding: 5px; @@ -41,16 +41,25 @@ } /* Buttons */ -#quizzerButtons button { +.quizzerButtons button { width: 75px; height: 25px; } /* Feedback */ -#quizzerFeedback { +.quizzerFeedback { margin-top: 15px; } -#quizzerFeedbackTerm { +.quizzerFeedbackTerm { text-decoration-line: underline; cursor: pointer; -} -\ No newline at end of file +} + +/* Congrats message */ +.congrats p { + margin: 20px; +} +.congrats button { + width: 100px; + height: 35px; +} diff --git a/css/reference.css b/css/reference.css @@ -12,7 +12,7 @@ main > h1 { } /* Controls */ -#controls { +.referenceTableControls { margin-top: 10px; margin-bottom: 15px; } @@ -20,13 +20,13 @@ main > h1 { /******** Reference Table styles ********/ -#referenceTable { +.referenceTable { text-align: left; overflow-x: auto; } /* Header cells */ -#referenceTable th { +.referenceTable th { background-color: var(--hover-color); border: 1px solid var(--border-color); position: sticky; @@ -35,7 +35,7 @@ main > h1 { } /* All cells */ -#referenceTable td { +.referenceTable td { border: 1px solid var(--border-color); cursor: pointer; } @@ -51,11 +51,11 @@ main > h1 { --hover-color: #FFFFFF; } - header, h1, #controls, label, br { + header, h1, .referenceTableControls, label, br { display: none; } - - #referenceTable { + + .referenceTable { overflow: visible; } diff --git a/css/settingsInput.css b/css/settingsInput.css @@ -1,15 +0,0 @@ -.settingsInput h2 { - font-size: 20px; - margin-bottom: 5px; -} -.settingsInput { - margin-left: 5px; - margin-right: 5px; - margin-top: 10px; -} -.settingsInput div { - text-align: left; -} -.settingsInput input[type=number] { - width: 50px; -} diff --git a/images/x.svg b/images/x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg> +\ No newline at end of file diff --git a/index.html b/index.html @@ -9,67 +9,34 @@ <link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png"> <link rel="stylesheet" href="css/global.css"> <link rel="stylesheet" href="css/app.css"> - <link rel="stylesheet" href="css/filterInput.css"> - <link rel="stylesheet" href="css/settingsInput.css"> + <link rel="stylesheet" href="css/filtersPage.css"> <link rel="stylesheet" href="css/quizzer.css"> + <link rel="stylesheet" href="css/reference.css"> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script> + <script src="https://cdn.jsdelivr.net/npm/vue-router@3.5.1"></script> <script src="https://unpkg.com/papaparse@5.1.1/papaparse.min.js"></script> <script src="js/global.js"></script> - <script src="js/app.js"></script> - <script src="js/filterInput.js"></script> - <script src="js/settingsInput.js"></script> <script src="js/filters.js"></script> + <script src="js/filtersPage.js"></script> <script src="js/quizzer.js"></script> + <script src="js/reference.js"></script> + <script src="js/app.js"></script> </head> <body onload="Load();"> <div id="app"> - <header @click="Back"> - <img id="backArrow" v-show="state != 'home'" src="images/arrow-left.svg"/> - Spanish-Quizzer - </header> - - <main> - <noscript> - <div id="jsError"> - <h1>You must have JavaScript enabled to run Spanish-Quizzer.</h1> - <p>Spanish-Quizzer uses JavaScript to load vocab, apply filters, and check your answers.</p> - <p>You can still view the <a href="data/">data sets</a>, but you can't use the Quizzer or the Reference Tables.</p> - </div> - </noscript> - - <div id="home" v-show="state === 'home'" hidden> - <h1>What do you want to study?</h1> - <div> - <button @click="category = 'verbs'; state = 'settings';">Conjugations</button> - <button @click="category = 'vocab'; state = 'settings';">Vocab</button> - </div> - <div> - <a href="reference.html">Or look something up in the Reference Tables</a> - </div> - </div> - - <div id="settings" v-show="state === 'settings'" hidden> - <filter-input :category="category" v-model="filters"></filter-input> - - <settings-input v-model="settings"></settings-input> - - <div class="settingButtons"> - <button class="settingsStart" @click="CreateSession();">Start</button> - <button class="settingsResume" @click="ResumeSession();">Resume</button> - </div> + <noscript> + <header>Spanish-Quizzer</header> + <div id="jsError"> + <h1>You must have JavaScript enabled to run Spanish-Quizzer.</h1> + <p>Spanish-Quizzer uses JavaScript to load vocab, apply filters, and check your answers.</p> + <p>You can still view the <a href="data/">data sets</a>, but you can't use the Quizzer or the Reference Tables.</p> </div> + </noscript> - <quizzer id="quizzer" v-if="state === 'quizzer'" - :starting-index="promptIndex" :starting-prompts="prompts" :settings="settings" - @new-prompt="updateProgress" @finished-prompts="state = 'congrats';"> - </quizzer> - - <div id="congrats" v-show="state === 'congrats'" hidden> - <p>Congratulations, You finished all of the prompts!</p> - <button @click="Back">Continue</button> - </div> - </main> + <keep-alive exclude="quizzerPage"> + <router-view @back="Back"></router-view> + </keep-alive> </div> </body> -</html> -\ No newline at end of file +</html> diff --git a/js/app.js b/js/app.js @@ -1,9 +1,78 @@ // Declare global variables -let Data; let app; +// page-header component +const pageHeader = Vue.component("pageHeader", { + props: { + image: { + type: String + }, + title: { + type: String, + default: "Spanish-Quizzer", + } + }, + template: ` + <header @click="$emit('back');"> + <img v-if="image" :src="image"/> + {{ title }} + </header> + ` +}); + + + +// App pages +const homePage = Vue.component("homePage", { + methods: { + /** + * Handle a keyup event (implements keyboard shortcuts). + * @param {object} e - The event args. + */ + keyup: function(e) { + if (this._inactive) return; + if (e.key === "c") this.$router.push("verbs"); + if (e.key === "v") this.$router.push("vocab"); + if (e.key === "r") this.$router.push("reference"); + }, + }, + created: function() { + // Add keyup handler + window.addEventListener("keyup", this.keyup); + }, + destroyed: function() { + // Remove keyup handler + window.removeEventListener("keyup", this.keyup); + }, + template: ` + <div class="home"> + <page-header></page-header> + <main> + <h1>What do you want to study?</h1> + <div> + <router-link tag="button" to="/verbs">Study Conjugations</router-link> + <router-link tag="button" to="/vocab">Study Vocab</router-link> + <router-link tag="button" to="/reference">Reference Tables</router-link> + </div> + </main> + </div> + `, +}); +const referencePage = Vue.component("referencePage", { + template: ` + <div> + <page-header @back="$emit('back');" image="images/arrow-left.svg"></page-header> + <main> + <reference-tables :data="this.$root.$data.data"></reference-tables> + </main> + </div> + `, +}); + + + /** * Initialize the Vue app */ @@ -11,124 +80,56 @@ function loadVue() { app = new Vue({ el: "#app", // Mount to app div - data: { - state: "home", // Can be either "home", "settings", or "quizzer" - category: "verbs", // Can be either "verbs" or "vocab" - filters: [], - settings: getSettings(), - prompts: [], - promptIndex: 0, - }, + router: new VueRouter({ + routes: [ + { path: "/", redirect: "/home" }, + { path: "/home", name: "home", component: homePage }, + { path: "/verbs", name: "verbs", component: filtersPage, props: {category: "verbs"}}, + { path: "/vocab", name: "vocab", component: filtersPage, props: {category: "vocab"}}, + { path: "/quizzer", name: "quizzer", component: quizzerPage, props:true }, + { path: "/reference", name: "reference", component: referencePage }, + ], + }), methods: { /** - * Return to the previous state. + * Go back to the previous page. */ Back: function() { - switch (app.state) { - case "quizzer": - case "congrats": - app.state = "settings"; - break; - case "settings": + switch (this.$route.name) { case "home": - default: - app.state = "home"; break; + case "verbs": + case "vocab": + case "reference": + this.$router.push("home"); + break; + case "quizzer": + this.$router.push(this.$route.params.referer || "home"); } }, /** - * Update the user's progress in localStorage. - * @param {Array} prompts - The list of prompts. - * @param {Number} index - The index of the current prompt. - */ - updateProgress: function(prompts, index) { - // Get localStorage prefix - let prefix = app.category === "verbs" ? "verb-" : "vocab-"; - - // Save progress to local storage - localStorage.setItem(prefix + "prompts", JSON.stringify(prompts)); - localStorage.setItem(prefix + "prompt", JSON.stringify(index)); - }, - - /** - * Start a new quizzer session - */ - CreateSession: function() { - // Get prompts - if (this.category === "vocab") { - this.prompts = Shuffle(ApplyFilters(Data.vocab, GetVocabFilters(this.filters), this.settings)); - } - else if (this.category === "verbs") { - // Get prompts - this.prompts = Shuffle(ApplyFilters(Data.verbs, GetVerbFilters(this.filters), this.settings)); - } - - // Set progress - this.promptIndex = 0; - - // Start quizzer - this.StartSession(); - }, - - /** - * Resume the previous quizzer session. + * Handle a keyup event (implements keyboard shortcuts). + * @param {object} e - The event args. */ - ResumeSession: function() { - // Get localStorage prefix - let prefix; - if (this.category === "vocab") { - prefix = "vocab-" - } - else if (this.category === "verbs") { - prefix = "verb-" - } - - // Load prompts and progress - this.prompts = JSON.parse(localStorage.getItem(prefix + "prompts")); - this.promptIndex = parseInt(localStorage.getItem(prefix + "prompt")); - - // Start quizzer - this.StartSession(); + keyup: function(e) { + if (e.key === "Escape") this.Back(); }, + }, - /** - * Perform validation checks and then start the quizzer. - */ - StartSession: function() { - // Validate prompts and promptIndex - if (!this.prompts) { - alert("An error occured while resuming the previous session."); - return; - } - else if (this.prompts.length === 0) { - alert("You must have at least one filter."); - return; - } - else if (isNaN(this.promptIndex) || this.promptIndex < 0 || this.promptIndex >= this.prompts.length) { - alert("An error occured while resuming the previous session."); - return; - } - - // Validate browser for voice input - if (this.settings.inputType !== "Text") { - if ((window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition) === undefined) { - alert("Your browser does not support voice input."); - return; - } - } + data: { + data: {} + }, - // Give iOS devices ringer warning for prompt audio - if (this.settings.promptType !== "Text") { - if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) { - alert("Please make sure your ringer is on in order to hear audio prompts."); - } - } + created: function() { + // Add keyup handler + window.addEventListener("keyup", this.keyup); + }, - // Show and hide elements (also enables the quizzer) - this.state = "quizzer"; - }, + destroyed: function() { + // Remove keyup handler + window.removeEventListener("keyup", this.keyup); }, }); } @@ -145,52 +146,6 @@ async function Load() { // Initialize the Vue app loadVue(); - // Unhide hidden divs - // Divs were hidden to improve interface for users with JS blocked - document.getElementById("home").hidden = false; - document.getElementById("settings").hidden = false; - document.getElementById("congrats").hidden = false; - - // Add event Listeners - document.addEventListener("keydown", KeyDown); - // Load Spanish-Quizzer data - Data = await loadData(); -} - - - -/** - * Handle a keyDown event (implements some keyboard shortcuts). - * @param {object} e - The event args. - */ -function KeyDown(e) { - if (e.key === "Escape") { - app.Back(); - } - - // Home shortcuts - if (app.state === "home") { - if (e.key === "c") { - app.category = "verbs"; - app.state = "settings"; - } - if (e.key === "v") { - app.category = "vocab"; - app.state = "settings"; - } - if (e.key === "r") { - window.location = "reference.html"; - } - } - - // Settings shortcuts - if (app.state === "settings") { - if (e.key === "s") { - app.CreateSession(); - } - if (e.key === "r") { - app.ResumeSession(); - } - } + app.data = await loadData(); } diff --git a/js/filterInput.js b/js/filterInput.js @@ -1,301 +0,0 @@ -let filterInput = Vue.component("filterInput", { - props: { - category: { - type: String, - default: "verbs", - }, - }, - - computed: { - value: function() { - if (this.category === "verbs") { - return this.verbFilters; - } - else if (this.category === "vocab") { - return this.vocabFilters; - } - } - }, - - data: function() { - return { - verbFilters: [], - vocabFilters: [], - }; - }, - - watch: { - value: { - handler: function(value) { - this.$emit("input", value); - }, - deep: true, - }, - }, - - methods: { - /** - * Add a filter to the settings page. - */ - AddFilter: function() { - if (this.category === "verbs") { - this.verbFilters.push({tense:"All Tenses", type:"All Types", subject:"All Subjects", direction:"Eng. → Conj."}); - } - else if (this.category === "vocab") { - this.vocabFilters.push({category:"All Categories", type:"All Types", direction:"Eng. ↔ Esp."}); - } - }, - - /** - * Remove a filter from the settings page. - * @param {Number} index - The index of the verb filter. - */ - RemoveFilter: function(index) { - if (this.category === "verbs") { - this.verbFilters.splice(index, 1); - } - else if (this.category === "vocab") { - this.vocabFilters.splice(index, 1); - } - }, - - /** - * Get the regularity filters available for a verb filter. - * @param {Number} index - The index of the verb filter. - * @returns {object} - An object with boolean properties for each regularity filter. - */ - getTenseTypes: function(index) { - // Get filter options - let filters = {"All Types":true, "Reflexive":true, "Regular":true, "Nonregular":true, "Stem Changing":true, "Orthographic":true, "Irregular":true} - switch(this.verbFilters[index].tense) - { - case "All Tenses": - break; - case "Present Participles": - filters["Reflexive"] = false; - filters["Orthographic"] = false; - break; - case "Past Participles": - filters["Reflexive"] = false; - filters["Stem Changing"] = false; - filters["Orthographic"] = false; - break; - case "Present Tense": - filters["Orthographic"] = false; - break; - case "Preterite Tense": - break; - case "Imperfect Tense": - filters["Stem Changing"] = false; - filters["Orthographic"] = false; - break; - case "Simple Future Tense": - filters["Stem Changing"] = false; - filters["Orthographic"] = false; - break; - case "Present Subjunctive Tense": - break; - } - - // Reset type if needed - if (!filters[this.verbFilters[index].type]) { - this.verbFilters[index].type = "All Types"; - } - - // Return filters - return filters; - }, - - /** - * Get the subject filters available for a verb filter. - * @param {Number} index - The index of the verb filter. - * @returns {object} - An object with boolean properties for each subject filter. - */ - getTenseSubjects: function(index) { - // Set default filters - let filters = {"All Subjects":true, "Type":true, "Yo":true, "Tú":true, "Él":true, "Nosotros":true, "Ellos":true} - - if (["Present Participles", "Past Participles"].includes(this.verbFilters[index].tense)) { - // Override filters - filters["Yo"] = false; - filters["Tú"] = false; - filters["Él"] = false; - filters["Nosotros"] = false; - filters["Ellos"] = false; - } - - // Reset subject - if (["Present Participles", "Past Participles"].includes(this.verbFilters[index].tense) && this.verbFilters[index].subject !== "Type") { - this.verbFilters[index].subject = "All Subjects"; - } - - // Return filters - return filters; - }, - - /** - * Get the filters available for a vocab category. - * @param {Number} index - The index of the vocab filter. - * @returns {Array} - An array containing available filters. - */ - getCategoryFilters: function(index) { - // Get filter options - let filters = {"All Types":true, "Adjectives":true, "Nouns":true, "Verbs":true} - switch(this.vocabFilters[index].category) - { - case "Verbs": - filters["Adjectives"] = false; - filters["Nouns"] = false; - filters["Verbs"] = false; - break; - - case "Adjectives": - filters["Nouns"] = false; - filters["Verbs"] = false; - break; - - case "Adverbs": - filters["Adjectives"] = false; - filters["Nouns"] = false; - filters["Verbs"] = false; - break; - - case "Prepositions": - case "Transitions": - case "Questions": - filters["Adjectives"] = false; - filters["Nouns"] = false; - filters["Verbs"] = false; - break; - - case "Colors": - filters["Nouns"] = false; - filters["Verbs"] = false; - break; - - case "Days": - case "Months": - case "Numbers": - filters["Adjectives"] = false; - filters["Verbs"] = false; - break; - - case "Weather": - case "Professions": - filters["Adjectives"] = false; - break; - - case "Family": - case "Clothes": - filters["Verbs"] = false; - break; - - case "Nature": - case "House": - case "Vacation": - case "Childhood": - case "Food": - case "Health": - break; - } - - // Reset type if needed - if (!filters[this.vocabFilters[index].type]) { - this.vocabFilters[index].type = "All Types"; - } - - // Return filters - return filters; - } - }, - - template: ` - <div class="filtersInput" ref="container"> - <div class="verbSettings" v-show="category === 'verbs'"> - <h1>Choose your settings and then click start.</h1> - - <h2> - Verb Filters - <button @click="AddFilter();">Add Filter</button> - </h2> - - <div v-for="(filter, index) in verbFilters" class="filter"> - <select v-model="filter.tense"> - <option>All Tenses</option> - <option>Present Participles</option> - <option>Past Participles</option> - <option>Present Tense</option> - <option>Preterite Tense</option> - <option>Imperfect Tense</option> - <option>Simple Future Tense</option> - <option>Present Subjunctive Tense</option> - </select> - <select v-model="filter.type"> - <option v-for="(available, type) in getTenseTypes(index)" :disabled="!available">{{ type }}</option> - </select> - <select v-model="filter.subject"> - <option v-for="(available, subject) in getTenseSubjects(index)" :disabled="!available">{{ subject }}</option> - </select> - <select v-model="filter.direction"> - <option>Eng. → Conj.</option> - <option>Esp. → Conj.</option> - <option>Conj. → Eng.</option> - <option>Conj. → Esp.</option> - </select> - <button class="itemRemove" @click="RemoveFilter(index);">╳</button> - </div> - </div> - - - <div class="vocabSettings" v-show="category === 'vocab'"> - <h1>Choose your settings and then click start.</h1> - - <h2> - Vocabulary Filters - <button @click="AddFilter();">Add Filter</button> - </h2> - - <div v-for="(filter, index) in vocabFilters" class="filter"> - <select class="vocabSetName" v-model="filter.category"> - <option>All Categories</option> - <optgroup label="Common Words"> - <option>Adjectives</option> - <option>Adverbs</option> - <option>Prepositions</option> - <option>Transitions</option> - <option>Verbs</option> - </optgroup> - <optgroup label="Basic Words"> - <option>Colors</option> - <option>Days</option> - <option>Months</option> - <option>Numbers</option> - <option>Questions</option> - </optgroup> - <optgroup label="Advanced Words"> - <option>Childhood</option> - <option>Clothes</option> - <option>Family</option> - <option>Food</option> - <option>Health</option> - <option>House</option> - <option>Nature</option> - <option>Professions</option> - <option>Vacation</option> - <option>Weather</option> - </optgroup> - </select> - <select v-model="filter.type"> - <option v-for="(available, type) in getCategoryFilters(index)" :disabled="!available">{{ type }}</option> - </select> - <select v-model="filter.direction"> - <option>Eng. ↔ Esp.</option> - <option>Eng. → Esp.</option> - <option>Esp. → Eng.</option> - </select> - <button class="itemRemove" @click="RemoveFilter(index);">╳</button> - </div> - </div> - </div> - `, -}); diff --git a/js/filtersPage.js b/js/filtersPage.js @@ -0,0 +1,511 @@ +// filter-input component +let filterInput = Vue.component("filterInput", { + props: { + category: { + type: String, + default: "verbs", + }, + }, + + computed: { + value: function() { + if (this.category === "verbs") { + return this.verbFilters; + } + else if (this.category === "vocab") { + return this.vocabFilters; + } + } + }, + + data: function() { + return { + verbFilters: [], + vocabFilters: [], + }; + }, + + watch: { + value: { + handler: function(value) { + this.$emit("input", value); + }, + deep: true, + }, + }, + + methods: { + /** + * Add a filter to the filters page. + */ + AddFilter: function() { + if (this.category === "verbs") { + this.verbFilters.push({tense:"All Tenses", type:"All Types", subject:"All Subjects", direction:"Eng. → Conj."}); + } + else if (this.category === "vocab") { + this.vocabFilters.push({category:"All Categories", type:"All Types", direction:"Eng. ↔ Esp."}); + } + }, + + /** + * Remove a filter from the filters page. + * @param {Number} index - The index of the verb filter. + */ + RemoveFilter: function(index) { + if (this.category === "verbs") { + this.verbFilters.splice(index, 1); + } + else if (this.category === "vocab") { + this.vocabFilters.splice(index, 1); + } + }, + + /** + * Get the regularity filters available for a verb filter. + * @param {Number} index - The index of the verb filter. + * @returns {object} - An object with boolean properties for each regularity filter. + */ + getTenseTypes: function(index) { + // Get filter options + let filters = {"All Types":true, "Reflexive":true, "Regular":true, "Nonregular":true, "Stem Changing":true, "Orthographic":true, "Irregular":true} + switch(this.verbFilters[index].tense) + { + case "All Tenses": + break; + case "Present Participles": + filters["Reflexive"] = false; + filters["Orthographic"] = false; + break; + case "Past Participles": + filters["Reflexive"] = false; + filters["Stem Changing"] = false; + filters["Orthographic"] = false; + break; + case "Present Tense": + filters["Orthographic"] = false; + break; + case "Preterite Tense": + break; + case "Imperfect Tense": + filters["Stem Changing"] = false; + filters["Orthographic"] = false; + break; + case "Simple Future Tense": + filters["Stem Changing"] = false; + filters["Orthographic"] = false; + break; + case "Present Subjunctive Tense": + break; + } + + // Reset type if needed + if (!filters[this.verbFilters[index].type]) { + this.verbFilters[index].type = "All Types"; + } + + // Return filters + return filters; + }, + + /** + * Get the subject filters available for a verb filter. + * @param {Number} index - The index of the verb filter. + * @returns {object} - An object with boolean properties for each subject filter. + */ + getTenseSubjects: function(index) { + // Set default filters + let filters = {"All Subjects":true, "Type":true, "Yo":true, "Tú":true, "Él":true, "Nosotros":true, "Ellos":true} + + if (["Present Participles", "Past Participles"].includes(this.verbFilters[index].tense)) { + // Override filters + filters["Yo"] = false; + filters["Tú"] = false; + filters["Él"] = false; + filters["Nosotros"] = false; + filters["Ellos"] = false; + } + + // Reset subject + if (["Present Participles", "Past Participles"].includes(this.verbFilters[index].tense) && this.verbFilters[index].subject !== "Type") { + this.verbFilters[index].subject = "All Subjects"; + } + + // Return filters + return filters; + }, + + /** + * Get the filters available for a vocab category. + * @param {Number} index - The index of the vocab filter. + * @returns {Array} - An array containing available filters. + */ + getCategoryFilters: function(index) { + // Get filter options + let filters = {"All Types":true, "Adjectives":true, "Nouns":true, "Verbs":true} + switch(this.vocabFilters[index].category) + { + case "Verbs": + filters["Adjectives"] = false; + filters["Nouns"] = false; + filters["Verbs"] = false; + break; + + case "Adjectives": + filters["Nouns"] = false; + filters["Verbs"] = false; + break; + + case "Adverbs": + filters["Adjectives"] = false; + filters["Nouns"] = false; + filters["Verbs"] = false; + break; + + case "Prepositions": + case "Transitions": + case "Questions": + filters["Adjectives"] = false; + filters["Nouns"] = false; + filters["Verbs"] = false; + break; + + case "Colors": + filters["Nouns"] = false; + filters["Verbs"] = false; + break; + + case "Days": + case "Months": + case "Numbers": + filters["Adjectives"] = false; + filters["Verbs"] = false; + break; + + case "Weather": + case "Professions": + filters["Adjectives"] = false; + break; + + case "Family": + case "Clothes": + filters["Verbs"] = false; + break; + + case "Nature": + case "House": + case "Vacation": + case "Childhood": + case "Food": + case "Health": + break; + } + + // Reset type if needed + if (!filters[this.vocabFilters[index].type]) { + this.vocabFilters[index].type = "All Types"; + } + + // Return filters + return filters; + } + }, + + template: ` + <div class="filtersInput" ref="container"> + <div class="verbSettings" v-show="category === 'verbs'"> + <h1>Choose your settings and then click start.</h1> + + <h2> + Verb Filters + <button @click="AddFilter();">Add Filter</button> + </h2> + + <div v-for="(filter, index) in verbFilters" class="filter"> + <select v-model="filter.tense"> + <option>All Tenses</option> + <option>Present Participles</option> + <option>Past Participles</option> + <option>Present Tense</option> + <option>Preterite Tense</option> + <option>Imperfect Tense</option> + <option>Simple Future Tense</option> + <option>Present Subjunctive Tense</option> + </select> + <select v-model="filter.type"> + <option v-for="(available, type) in getTenseTypes(index)" :disabled="!available">{{ type }}</option> + </select> + <select v-model="filter.subject"> + <option v-for="(available, subject) in getTenseSubjects(index)" :disabled="!available">{{ subject }}</option> + </select> + <select v-model="filter.direction"> + <option>Eng. → Conj.</option> + <option>Esp. → Conj.</option> + <option>Conj. → Eng.</option> + <option>Conj. → Esp.</option> + </select> + <button class="itemRemove" @click="RemoveFilter(index);">╳</button> + </div> + </div> + + + <div class="vocabSettings" v-show="category === 'vocab'"> + <h1>Choose your settings and then click start.</h1> + + <h2> + Vocabulary Filters + <button @click="AddFilter();">Add Filter</button> + </h2> + + <div v-for="(filter, index) in vocabFilters" class="filter"> + <select class="vocabSetName" v-model="filter.category"> + <option>All Categories</option> + <optgroup label="Common Words"> + <option>Adjectives</option> + <option>Adverbs</option> + <option>Prepositions</option> + <option>Transitions</option> + <option>Verbs</option> + </optgroup> + <optgroup label="Basic Words"> + <option>Colors</option> + <option>Days</option> + <option>Months</option> + <option>Numbers</option> + <option>Questions</option> + </optgroup> + <optgroup label="Advanced Words"> + <option>Childhood</option> + <option>Clothes</option> + <option>Family</option> + <option>Food</option> + <option>Health</option> + <option>House</option> + <option>Nature</option> + <option>Professions</option> + <option>Vacation</option> + <option>Weather</option> + </optgroup> + </select> + <select v-model="filter.type"> + <option v-for="(available, type) in getCategoryFilters(index)" :disabled="!available">{{ type }}</option> + </select> + <select v-model="filter.direction"> + <option>Eng. ↔ Esp.</option> + <option>Eng. → Esp.</option> + <option>Esp. → Eng.</option> + </select> + <button class="itemRemove" @click="RemoveFilter(index);">╳</button> + </div> + </div> + </div> + `, +}); + + + +// settings-input component +let settingsInput = Vue.component("settingsInput", { + props: { + value: { + type: Object, + default: getSettings(), + }, + }, + + watch: { + value: { + handler: function(value) { + setSettings(value); + + this.$emit("input", value); + }, + deep: true, + }, + }, + + template: ` + <div class="settingsInput" ref="container"> + <h2>Quizzer Settings</h2> + <div> + <input type="checkbox" id="settingsDarkTheme" v-model="value.darkTheme"> + <label for="settingsDarkTheme">Dark Mode</label> + </div> + <div> + <label for="settingsPromptType">Prompt type</label> + <select id="settingsPromptType" v-model="value.promptType"> + <option>Text</option> + <option>Audio</option> + <option>Both</option> + </select> + </div> + <div> + <label for="settingsInputType">Input type</label> + <select id="settingsInputType" v-model="value.inputType"> + <option>Text</option> + <option>Voice</option> + <option>Either</option> + </select> + </div> + <div> + <label for="settingsOnMissedPrompt">When I miss a prompt</label> + <select id="settingsOnMissedPrompt" v-model="value.onMissedPrompt"> + <option>Correct me</option> + <option>Tell me</option> + <option>Ignore it</option> + </select> + </div> + <div> + <label for="settingsRepeatPrompts">Repeat missed prompts</label> + <select id="settingsRepeatPrompts" v-model="value.repeatPrompts"> + <option>Never</option> + <option>Immediately</option> + <option>5 prompts later</option> + <option>5 & 10 prompts later</option> + <option>At the end</option> + </select> + </div> + <div> + <label for="settingsMultiplePrompts">Multiple prompts</label> + <select id="settingsMultiplePrompts" v-model="value.multiplePrompts"> + <option>Show together</option> + <option>Show separately</option> + <option>Show one</option> + </select> + </div> + <div> + <label for="settingsMultipleAnswers">Multiple answers</label> + <select id="settingsMultipleAnswers" v-model="value.multipleAnswers"> + <option>Require all</option> + <option>Require any</option> + </select> + </div> + <div> + <input type="checkbox" id="settingsRemoveDuplicates" v-model="value.removeDuplicates"> + <label for="settingsRemoveDuplicates">Remove duplicate prompts</label> + </div> + </div> + `, +}); + + + +// filters-page component +let filtersPage = Vue.component("filtersPage", { + props: { + category: { + type: String, + default: "verbs", + } + }, + + data: function() { + return { + filters: [], + settings: getSettings(), + }; + }, + + methods: { + /** + * Start a new quizzer session + */ + CreateSession: function() { + // Get prompts + if (this.category === "vocab") { + prompts = Shuffle(ApplyFilters(this.$root.$data.data.vocab, GetVocabFilters(this.filters), this.settings)); + } + else if (this.category === "verbs") { + // Get prompts + prompts = Shuffle(ApplyFilters(this.$root.$data.data.verbs, GetVerbFilters(this.filters), this.settings)); + } + + // Set progress + promptIndex = 0; + + // Start quizzer + this.StartSession(prompts, promptIndex); + }, + + /** + * Resume the previous quizzer session. + */ + ResumeSession: function() { + // Load prompts and progress + let { prompts, index } = JSON.parse(localStorage.getItem("last-session")); + + // Start quizzer + this.StartSession(prompts, index); + }, + + /** + * Perform validation checks and then start the quizzer. + */ + StartSession: function(prompts, promptIndex) { + // Validate prompts and promptIndex + if (!prompts) { + alert("An error occured while resuming the previous session."); + return; + } + else if (prompts.length === 0) { + alert("You must have at least one filter."); + return; + } + else if (isNaN(promptIndex) || promptIndex < 0 || promptIndex >= prompts.length) { + alert("An error occured while resuming the previous session."); + return; + } + + // Validate browser for voice input + if (this.settings.inputType !== "Text") { + if ((window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition) === undefined) { + alert("Your browser does not support voice input."); + return; + } + } + + // Give iOS devices ringer warning for prompt audio + if (this.settings.promptType !== "Text") { + if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) { + alert("Please make sure your ringer is on in order to hear audio prompts."); + } + } + + // Start quizzer + this.$router.push({name:"quizzer", params:{startingPrompts:prompts, startingIndex:promptIndex, settings:this.settings, referer:this.category}}); + }, + + /** + * Handle a keyup event (implements some keyboard shortcuts). + * @param {object} e - The event args. + */ + keyup: function(e) { + if (this._inactive) return; + if (e.key === "s") this.CreateSession(); + if (e.key === "r") this.ResumeSession(); + } + }, + + created: function() { + // Add keyup handler + window.addEventListener("keyup", this.keyup); + }, + + destroyed: function() { + // Remove keyup handler + window.removeEventListener("keyup", this.keyup); + }, + + template: ` + <div class="filtersPage"> + <page-header @back="$emit('back');" image="images/arrow-left.svg"></page-header> + <main> + <filter-input :category="category" v-model="filters"></filter-input> + <settings-input v-model="settings"></settings-input> + <div class="settingButtons"> + <button class="settingsStart" @click="CreateSession();">Start</button> + <button class="settingsResume" @click="ResumeSession();">Resume</button> + </div> + </main> + </div> + `, +}); diff --git a/js/global.js b/js/global.js @@ -23,22 +23,6 @@ function SetTheme(darkTheme = null) { /** - * Get the language code that matches a label. - * @param {String} label - The label. - * @returns {String} - The language code ("en", "es", etc.) - */ -function getLang(label) { - if (label.toLowerCase().includes("spanish")) { - return "es"; - } - else { - return "en"; - } -} - - - -/** * Read a peice of text. * @param {String} text - The text to read. * @param {String} label - The language of the text. diff --git a/js/quizzer.js b/js/quizzer.js @@ -60,6 +60,12 @@ let quizzer = Vue.component("quizzer", { * Give the user the next prompt and reset the quizzer. */ Reset: function() { + // Get new prompt + this.index++; + if (this.index >= this.prompts.length) { + return; + } + // Show and hide elements this.responceActive = true; try { @@ -68,14 +74,6 @@ let quizzer = Vue.component("quizzer", { } catch { } - // Get new prompt - this.index++; - if (this.index === this.prompts.length) { - // The user just finished - this.$emit("finished-prompts"); - return; - } - // Emit new-prompt event this.$emit("new-prompt", this.prompts, this.index); @@ -224,6 +222,20 @@ let quizzer = Vue.component("quizzer", { this.Continue(); } }, + + /** + * Get the language code that matches a label. + * @param {String} label - The label. + * @returns {String} - The language code ("en", "es", etc.) + */ + getLang: function(label) { + if (label.toLowerCase().includes("spanish")) { + return "es"; + } + else { + return "en"; + } + }, }, computed: { @@ -244,7 +256,7 @@ let quizzer = Vue.component("quizzer", { created: function() { // Add keyup handler window.addEventListener("keyup", this.keyup); - + // Update prompts this.prompts = this.startingPrompts; this.index = this.startingIndex - 1; @@ -260,34 +272,91 @@ let quizzer = Vue.component("quizzer", { template: ` <div> - <p id="quizzerProgress">{{ index }} / {{ prompts.length }}</p> - - <section> - <label id="quizzerPromptType" for="quizzerPrompt">{{ prompt[0] }}</label> - <span id="quizzerPrompt" :lang="getLang(prompt[0])" @click="Read(prompt[1], prompt[0]);">{{ settings.promptType === "Audio" ? "Click to hear again" : prompt[1] }}</span> - </section> - - <section> - <label id="quizzerInputType" for="quizzerInput">{{ prompt[2] }}</label> - <input id="quizzerInput" ref="input" type="text" v-model="responce" :readonly="!responceActive || settings.inputType === 'Voice'" - :lang="getLang(prompt[2])" autocomplete="off" spellcheck="false" autocorrect="off" placeholder="Type the answer"> - </section> - - <div id="quizzerButtons"> - <button v-if="responceActive" :disabled="settings.inputType === 'Voice'" @click="Submit();">Submit</button> - <button v-else @click="Continue();">Continue</button> - <button @click="Reset();">Skip</button> + <div class="quizzer" v-show="index < prompts.length"> + <p class="quizzerProgress">{{ index }} / {{ prompts.length }}</p> + + <section> + <label class="quizzerPromptType" for="quizzerPrompt">{{ prompt[0] }}</label> + <span class="quizzerPrompt" :lang="getLang(prompt[0])" @click="Read(prompt[1], prompt[0]);">{{ settings.promptType === "Audio" ? "Click to hear again" : prompt[1] }}</span> + </section> + + <section> + <label class="quizzerInputType" for="quizzerInput">{{ prompt[2] }}</label> + <input class="quizzerInput" ref="input" type="text" v-model="responce" :readonly="!responceActive || settings.inputType === 'Voice'" + :lang="getLang(prompt[2])" autocomplete="off" spellcheck="false" autocorrect="off" placeholder="Type the answer"> + </section> + + <div class="quizzerButtons"> + <button v-if="responceActive" :disabled="settings.inputType === 'Voice'" @click="Submit();">Submit</button> + <button v-else @click="Continue();">Continue</button> + <button @click="Reset();">Skip</button> + </div> + + <div class="quizzerFeedback bad" ref="feedback" v-show="!responceActive"> + <span v-if="settings.onMissedPrompt === 'Correct me'"> + The correct answer is + <span class="quizzerFeedbackTerm" @click="Read(prompt[3], prompt[2]);">{{ prompt[3].toLowerCase() }}</span>. + </span> + <span v-if="settings.onMissedPrompt === 'Tell me'"> + Incorrect. + </span> + </div> </div> - <div id="quizzerFeedback" ref="feedback" v-show="!responceActive" class="bad"> - <span v-if="settings.onMissedPrompt === 'Correct me'"> - The correct answer is - <span id="quizzerFeedbackTerm" @click="Read(prompt[3], prompt[2]);">{{ prompt[3].toLowerCase() }}</span>. - </span> - <span v-if="settings.onMissedPrompt === 'Tell me'"> - Incorrect. - </span> + <div class="congrats" v-show="index >= prompts.length"> + <p>Congratulations, You finished all of the prompts!</p> + <button @click="$emit('finished-prompts')">Continue</button> </div> </div> `, }); + + + +// quizzer-page component +const quizzerPage = Vue.component("quizzerPage", { + props: { + "referer": { + type: String, + default: "home", + }, + "startingPrompts": { + type: Array + }, + "startingIndex": { + type: Number + }, + "settings": { + type: Object + } + }, + + methods: { + /** + * Update the user's progress in localStorage. + * @param {Array} prompts - The list of prompts. + * @param {Number} index - The index of the current prompt. + */ + updateProgress: function(prompts, index) { + // Save progress + localStorage.setItem("last-session", JSON.stringify({ prompts: prompts, index: index })); + } + }, + + mounted: function() { + if (this.startingPrompts == null || this.startingIndex == null || this.settings == null) { + this.$router.replace({name:this.referer}); + } + }, + + template: ` + <div class="quizzer-page"> + <page-header @back="$emit('back', referer);" image="images/x.svg"></page-header> + <main> + <quizzer :starting-prompts="startingPrompts" :starting-index="startingIndex" :settings="settings" + @new-prompt="updateProgress" @finished-prompts="$emit('back', referer);"> + </quizzer> + </main> + </div> + `, +}); diff --git a/js/reference.js b/js/reference.js @@ -1,54 +1,91 @@ -// Declare global variables -let app; - - - -/** - * Initializes the Vue app - */ -function loadVue() { - app = new Vue({ - el: "#app", // Mount to app div - +let referenceTables = Vue.component("referenceTables", { + props: { data: { + type: Object, + default: {}, + } + }, + data: function() { + return { category: "Choose a category", - data: {"Choose a category":[]}, - query: "" + tableData: {...{"Choose a category":[]}, ...this.data}, + query: "", } - }); -} - - - -/** - * Load the document - */ -async function Load() { - // Update theme - SetTheme(null); - - // Initialize the Vue - loadVue(); + }, + methods: { + /** + * Set the table height. + */ + setTableHeight: function() { + this.$refs.referenceTable.style.height = `${window.innerHeight - this.$refs.referenceTable.offsetTop - 10}px`; + }, - // Unhide hidden divs - // Divs were hidden to improve interface for users with JS blocked - document.querySelector("h1").hidden = false; - document.getElementById("controls").hidden = false; - document.getElementById("referenceTable").hidden = false; + /** + * Get the language code that matches a label. + * @param {String} label - The label. + * @returns {String} - The language code ("en", "es", etc.) + */ + getLang: function(label) { + if (label.toLowerCase().includes("spanish")) { + return "es"; + } + else { + return "en"; + } + }, - // Set table height - setTableHeight(); + /** + * Handle a keyup event (implements keyboard shortcuts). + * @param {object} e - The event args. + */ + keyup: function(e) { + if (this._inactive) return; + if (e.key === "h" || e.key == "/") { + try { + this.$refs.search.focus(); + } catch {} + } + if (e.key === "c") this.category = "verbs" + if (e.key === "v") this.category = "vocab"; + }, + }, + mounted: function() { + // Set table height + this.setTableHeight(); - // Load Spanish-Quizzer data - app.data = {...app.data, ...await loadData()}; -} + // Add onresize handler + window.addEventListener("resize", this.setTableHeight); + // Add keyup handler + window.addEventListener("keyup", this.keyup); + }, + destroyed: function() { + // Remove onresize handler + window.removeEventListener("resize", this.setTableHeight); + // Remove keyup handler + window.removeEventListener("keyup", this.keyup); + }, + template: ` + <div> + <div class="referenceTableControls"> + <select aria-label="Vocab Set" v-model="category"> + <option>Choose a category</option> + <option value="verbs">Conjugations</option> + <option value="vocab">Vocab</option> + </select> + <input type="text" aria-label="Search" v-model="query" placeholder="Search" + autocomplete="off" autocorrect="off" ref="search"> + </div> -/** - * Set the table height. - */ -function setTableHeight() { - var tableY = document.getElementById("referenceTable").offsetTop; - document.getElementById("referenceTable").style.height = `${window.innerHeight - tableY - 10}px`; -} + <div class="referenceTable" ref="referenceTable"> + <table> + <tr v-for="(row, rowIndex) in data[category]" v-show="rowIndex === 0 || row.join(',').toLowerCase().includes(query.toLowerCase())"> + <th v-if="rowIndex === 0" v-for="column in row">{{ column }}</th> + <td v-if="rowIndex !== 0" v-for="(column, columnIndex) in row" @click="Read(column, data[category][0][columnIndex])" :lang="getLang(data[category][0][columnIndex])">{{ column }}</td> + </tr> + </table> + </div> + </div> + ` +}); diff --git a/js/settingsInput.js b/js/settingsInput.js @@ -1,82 +0,0 @@ -let settingsInput = Vue.component("settingsInput", { - props: { - value: { - type: Object, - default: getSettings(), - }, - }, - - watch: { - value: { - handler: function(value) { - setSettings(value); - - this.$emit("input", value); - }, - deep: true, - }, - }, - - template: ` - <div class="settingsInput" ref="container"> - <h2>Quizzer Settings</h2> - <div> - <input type="checkbox" id="settingsDarkTheme" v-model="value.darkTheme"> - <label for="settingsDarkTheme">Dark Mode</label> - </div> - <div> - <label for="settingsPromptType">Prompt type</label> - <select id="settingsPromptType" v-model="value.promptType"> - <option>Text</option> - <option>Audio</option> - <option>Both</option> - </select> - </div> - <div> - <label for="settingsInputType">Input type</label> - <select id="settingsInputType" v-model="value.inputType"> - <option>Text</option> - <option>Voice</option> - <option>Either</option> - </select> - </div> - <div> - <label for="settingsOnMissedPrompt">When I miss a prompt</label> - <select id="settingsOnMissedPrompt" v-model="value.onMissedPrompt"> - <option>Correct me</option> - <option>Tell me</option> - <option>Ignore it</option> - </select> - </div> - <div> - <label for="settingsRepeatPrompts">Repeat missed prompts</label> - <select id="settingsRepeatPrompts" v-model="value.repeatPrompts"> - <option>Never</option> - <option>Immediately</option> - <option>5 prompts later</option> - <option>5 & 10 prompts later</option> - <option>At the end</option> - </select> - </div> - <div> - <label for="settingsMultiplePrompts">Multiple prompts</label> - <select id="settingsMultiplePrompts" v-model="value.multiplePrompts"> - <option>Show together</option> - <option>Show separately</option> - <option>Show one</option> - </select> - </div> - <div> - <label for="settingsMultipleAnswers">Multiple answers</label> - <select id="settingsMultipleAnswers" v-model="value.multipleAnswers"> - <option>Require all</option> - <option>Require any</option> - </select> - </div> - <div> - <input type="checkbox" id="settingsRemoveDuplicates" v-model="value.removeDuplicates"> - <label for="settingsRemoveDuplicates">Remove duplicate prompts</label> - </div> - </div> - `, -}); diff --git a/reference.html b/reference.html @@ -1,57 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"/> - <title>Reference Tables - Spanish-Quizzer</title> - <meta name="description" content="Look up Spanish vocabulary and conjugations in free and printable reference tables."> - <meta name="viewport" content="width=device-width, user-scalable=no"/> - <link rel="icon" type="image/png" href="images/favicon-32.png"> - <link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png"> - <link rel="stylesheet" href="css/global.css"> - <link rel="stylesheet" href="css/reference.css"> - <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script> - <script src="https://unpkg.com/papaparse@5.1.1/papaparse.min.js"></script> - <script src="js/global.js"></script> - <script src="js/reference.js"></script> - </head> - - <body onload="Load()" onresize="setTableHeight()"> - <div id="app"> - <header onclick="window.location='./'"> - <img id="backArrow" src="images/arrow-left.svg"/> - Spanish-Quizzer - </header> - - <main> - <noscript> - <div id="jsError"> - <h1>You must have JavaScript enabled to run Spanish-Quizzer.</h1> - <p>Spanish-Quizzer uses JavaScript to load vocab, apply filters, and check your answers.</p> - <p>You can still view the <a href="data/">data sets</a>, but you can't use the Quizzer or the Reference Tables.</p> - </div> - </noscript> - - <h1 hidden>Reference Tables</h1> - - <div id="controls" hidden> - <select aria-label="Vocab Set" v-model="category"> - <option>Choose a category</option> - <option value="verbs">Conjugations</option> - <option value="vocab">Vocab</option> - </select> - <input type="text" aria-label="Search" v-model="query" placeholder="Search" - autocomplete="off" autocorrect="off"> - </div> - - <div id="referenceTable" hidden> - <table> - <tr v-for="(row, rowIndex) in data[category]" v-show="rowIndex === 0 || row.join(',').toLowerCase().includes(query.toLowerCase())"> - <th v-if="rowIndex === 0" v-for="column in row">{{ column }}</th> - <td v-if="rowIndex !== 0" v-for="(column, columnIndex) in row" @click="Read(column, data[category][0][columnIndex])" :lang="getLang(data[category][0][columnIndex])">{{ column }}</td> - </tr> - </table> - </div> - </main> - </div> - </body> -</html> -\ No newline at end of file diff --git a/tests/index.html b/tests/index.html @@ -17,9 +17,7 @@ <!-- Scripts being tested --> <script src="../js/global.js"></script> - <script src="../js/app.js"></script> - <script src="../js/filterInput.js"></script> - <script src="../js/settingsInput.js"></script> + <script src="../js/filtersPage.js"></script> <script src="../js/filters.js"></script> <script src="../js/quizzer.js"></script> @@ -32,9 +30,7 @@ <!-- Tests --> <script src="test.global.js"></script> - <script src="test.app.js"></script> - <script src="test.filterInput.js"></script> - <script src="test.settingsInput.js"></script> + <script src="test.filtersPage.js"></script> <script src="test.filters.js"></script> <script src="test.quizzer.js"></script> diff --git a/tests/test.app.js b/tests/test.app.js @@ -1,81 +0,0 @@ -describe("App", function() { - beforeEach(function() { - // Initialize Vue - loadVue(); - }); - - describe("Created lifecycle hook", function() { - it("State should be 'home'", function() { - expect(app.state).to.equal("home"); - }); - - it("Category should be 'verbs'", function() { - expect(app.category).to.equal("verbs"); - }); - - it("Settings should be correctly loaded", function() { - expect(app.settings).to.deep.equal(getSettings()); - }); - - it("Prompts should be empty", function() { - expect(app.prompts.length).to.equal(0); - }); - - it("PromptIndex should be 0", function() { - expect(app.promptIndex).to.equal(0); - }); - }); - - describe("Back method", function() { - it("Should go to home by default", function() { - // Set state and test Back method - app.state = "home"; - app.Back(); - expect(app.state).to.equal("home"); - - // Set state and test Back method again - app.state = ""; - app.Back(); - expect(app.state).to.equal("home"); - - // Set state and test Back method again - app.state = "test"; - app.Back(); - expect(app.state).to.equal("home"); - }); - - it("Should go to home when on settings", function() { - // Set state and test Back method - app.state = "settings"; - app.Back(); - expect(app.state).to.equal("home"); - }); - - it("Should go to settings when on quizzer", function() { - // Set state and test Back method - app.state = "quizzer"; - app.Back(); - expect(app.state).to.equal("settings"); - }); - }); - - describe("StartSession method", function() { - it("Should set state to 'quizzer'", function() { - // Initialize settings - app.state = "settings"; - app.settings.inputType = "Text"; - app.settings.promptType = "Text"; - app.prompts = ["prompt1", "prompt2", "prompt3"]; - - // Call StartSession - app.StartSession([1, 2, 3], 4, { - promptType: "Text", // Required to prevent browser validation alerts - inputType: "Text", // Required to prevent browser validation alerts - testSetting: "testValue", - }); - - // Assert parameters imported - expect(app.state).to.equal("quizzer"); - }); - }); -}); diff --git a/tests/test.filterInput.js b/tests/test.filterInput.js @@ -1,378 +0,0 @@ -describe("FilterInput", function() { - let FilterInput; - beforeEach(function() { - // Create filtersInput component - FilterInput = new filterInput(); - }); - - describe("Created lifecycle hook", function() { - it("Category should be 'verbs'", function() { - expect(FilterInput.category).to.equal("verbs"); - }); - - it("VerbFilters should be empty", function() { - expect(FilterInput.verbFilters.length).to.equal(0); - }); - - it("VocabFilters should be empty", function() { - expect(FilterInput.vocabFilters.length).to.equal(0); - }); - }); - - describe("Value computed property", function() { - it("Should return verb filters if category is 'verbs'", function() { - // Initialize variables - FilterInput.category = "verbs"; - FilterInput.verbFilters = "verb-filters"; - FilterInput.vocabFilters = "vocab-filters"; - - // Assert value returns verb filters - expect(FilterInput.value, "verb-filters"); - }); - - it("Should return vocab filters if category is 'vocab'", function() { - // Initialize variables - FilterInput.category = "vocab"; - FilterInput.verbFilters = "verb-filters"; - FilterInput.vocabFilters = "vocab-filters"; - - // Assert value returns vocab filters - expect(FilterInput.value, "vocab-filters"); - }); - }); - - describe("Value watch", function() { - it("Should emit input event", async function() { - // Initialize variables - FilterInput.category = "verbs"; - FilterInput.verbFilters = ["filter1"]; - - // Override $emit method - let event = ""; - let event_args; - FilterInput.$emit = function(name, value) { - event = name; - event_args = value; - }; - - // Edit verb filters - FilterInput.verbFilters.push("filter2"); - await FilterInput.$nextTick(); - - // Assert event emited - expect(event).to.equal("input"); - expect(event_args).to.have.deep.members(["filter1", "filter2"]); - }); - }); - - describe("AddFilter method", function() { - it("Should add a verb filter if category is 'verbs'", function() { - // Initialize variables - FilterInput.category = "verbs"; - expect(FilterInput.verbFilters.length).to.equal(0); - expect(FilterInput.vocabFilters.length).to.equal(0); - - // Add filter - FilterInput.AddFilter(); - - // Assert filter added - expect(FilterInput.verbFilters).to.have.deep.members([ - {tense:"All Tenses", type:"All Types", subject:"All Subjects", direction:"Eng. → Conj."}, - ]); - expect(FilterInput.vocabFilters).to.have.deep.members([]); - }); - - it("Should add a vocab filter if category is 'vocab'", function() { - // Initialize variables - FilterInput.category = "vocab"; - expect(FilterInput.verbFilters.length).to.equal(0); - expect(FilterInput.vocabFilters.length).to.equal(0); - - // Add filter - FilterInput.AddFilter(); - - // Assert filter added - expect(FilterInput.vocabFilters).to.have.deep.members([ - {category:"All Categories", type:"All Types", direction:"Eng. ↔ Esp."}, - ]); - expect(FilterInput.verbFilters).to.have.deep.members([]); - }); - }); - - describe("RemoveFilter method", function() { - it("Should remove the specified verb filter", function() { - // Initialize filters - FilterInput.category = "verbs"; - FilterInput.verbFilters = [ - "verb1", - "verb2", - "verb3", - ]; - FilterInput.vocabFilters = [ - "vocab1", - "vocab2", - "vocab3", - ]; - - // Remove filter - FilterInput.RemoveFilter(1); - - // Assert filter removed - expect(FilterInput.verbFilters.length).to.equal(2); - expect(FilterInput.verbFilters[0]).to.equal("verb1"); - expect(FilterInput.verbFilters[1]).to.equal("verb3"); - expect(FilterInput.vocabFilters.length).to.equal(3); - }); - - it("Should remove the specified vocab filter", function() { - // Initialize filters - FilterInput.category = "vocab"; - FilterInput.verbFilters = [ - "verb1", - "verb2", - "verb3", - ]; - FilterInput.vocabFilters = [ - "vocab1", - "vocab2", - "vocab3", - ]; - - // Remove filter - FilterInput.RemoveFilter(1); - - // Assert filter removed - expect(FilterInput.verbFilters.length).to.equal(3); - expect(FilterInput.vocabFilters.length).to.equal(2); - expect(FilterInput.vocabFilters[0]).to.equal("vocab1"); - expect(FilterInput.vocabFilters[1]).to.equal("vocab3"); - }); - }); - - describe("GetTenseTypes method", function() { - it("Should be correct for All Tenses", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"All Types", type:"All Types"} - ] - - // Get filters - let filters = FilterInput.getTenseTypes(0); - - // Assert filters are correct - expect(filters["All Types"]).to.equal(true); - expect(filters["Reflexive"]).to.equal(true); - expect(filters["Regular"]).to.equal(true); - expect(filters["Nonregular"]).to.equal(true); - expect(filters["Stem Changing"]).to.equal(true); - expect(filters["Orthographic"]).to.equal(true); - expect(filters["Irregular"]).to.equal(true); - }); - - it("Should be correct for Present Tense", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"Present Tense", type:"All Types"} - ] - - // Get filters - let filters = FilterInput.getTenseTypes(0); - - // Assert filters are correct - expect(filters["All Types"]).to.equal(true); - expect(filters["Reflexive"]).to.equal(true); - expect(filters["Regular"]).to.equal(true); - expect(filters["Nonregular"]).to.equal(true); - expect(filters["Stem Changing"]).to.equal(true); - expect(filters["Orthographic"]).to.equal(false); - expect(filters["Irregular"]).to.equal(true); - }); - - it("Should change selection if not available", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"Present Tense", type:"Orthographic"} - ] - - // Get filters - let filters = FilterInput.getTenseTypes(0); - - // Assert filters are correct - expect(filters["All Types"]).to.equal(true); - expect(filters["Reflexive"]).to.equal(true); - expect(filters["Regular"]).to.equal(true); - expect(filters["Nonregular"]).to.equal(true); - expect(filters["Stem Changing"]).to.equal(true); - expect(filters["Orthographic"]).to.equal(false); - expect(filters["Irregular"]).to.equal(true); - - // Assert selection changed - expect(FilterInput.verbFilters[0]["type"]).to.equal("All Types"); - }); - - it("Should not change selection if available", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"Preterite Tense", type:"Orthographic"} - ] - - // Get filters - let filters = FilterInput.getTenseTypes(0); - - // Assert filters are correct - expect(filters["All Types"]).to.equal(true); - expect(filters["Reflexive"]).to.equal(true); - expect(filters["Regular"]).to.equal(true); - expect(filters["Nonregular"]).to.equal(true); - expect(filters["Stem Changing"]).to.equal(true); - expect(filters["Orthographic"]).to.equal(true); - expect(filters["Irregular"]).to.equal(true); - - // Assert selection not changed - expect(FilterInput.verbFilters[0]["type"]).to.equal("Orthographic"); - }); - }); - - describe("GetTenseSubjects method", function() { - it("Should be correct for All Tenses", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"All Types", type:"All Types"} - ] - - // Get filters - let filters = FilterInput.getTenseSubjects(0); - - // Assert filters are correct - expect(filters["All Subjects"]).to.equal(true); - expect(filters["Yo"]).to.equal(true); - expect(filters["Tú"]).to.equal(true); - expect(filters["Él"]).to.equal(true); - expect(filters["Nosotros"]).to.equal(true); - expect(filters["Ellos"]).to.equal(true); - }); - - it("Should be correct for Present Participles", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"Present Participles", subject:"All Subjects", type:"All Types"} - ] - - // Get filters - let filters = FilterInput.getTenseSubjects(0); - - // Assert filters are correct - expect(filters["All Subjects"]).to.equal(true); - expect(filters["Yo"]).to.equal(false); - expect(filters["Tú"]).to.equal(false); - expect(filters["Él"]).to.equal(false); - expect(filters["Nosotros"]).to.equal(false); - expect(filters["Ellos"]).to.equal(false); - }); - - it("Should change selection if not available", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"Present Participles", subject:"Yo", type:"All Types"} - ] - - // Get filters - let filters = FilterInput.getTenseSubjects(0); - - // Assert filters are correct - expect(filters["All Subjects"]).to.equal(true); - expect(filters["Yo"]).to.equal(false); - expect(filters["Tú"]).to.equal(false); - expect(filters["Él"]).to.equal(false); - expect(filters["Nosotros"]).to.equal(false); - expect(filters["Ellos"]).to.equal(false); - - // Assert selection changed - expect(FilterInput.verbFilters[0]["subject"]).to.equal("All Subjects"); - }); - - it("Should not change selection if available", function() { - // Initialize filters - FilterInput.verbFilters = [ - {tense:"Present Participles", subject:"Type", type:"All Types"}, - {tense:"Preterite Tense", subject:"Yo", type:"All Types"}, - ] - - // Get filters - FilterInput.getTenseSubjects(0); - FilterInput.getTenseSubjects(1); - - // Assert selection not changed - expect(FilterInput.verbFilters[0].subject).to.equal("Type"); - expect(FilterInput.verbFilters[1].subject).to.equal("Yo"); - }); - }); - - describe("GetCategoryFilters method", function() { - it("Should be correct for Verbs", function() { - // Initialize filters - FilterInput.vocabFilters = [ - {category:"Verbs", type:"All Definitions"} - ] - - // Get filters - let filters = FilterInput.getCategoryFilters(0); - - // Assert filters are correct - expect(filters["All Types"]).to.equal(true); - expect(filters["Adjectives"]).to.equal(false); - expect(filters["Nouns"]).to.equal(false); - expect(filters["Verbs"]).to.equal(false); - }); - - it("Should be correct for sets with 1 type", function() { - // Initialize filters - FilterInput.vocabFilters = [ - {category:"Colors", type:"All Definitions"} - ] - - // Get filters - let filters = FilterInput.getCategoryFilters(0); - - // Assert filters are correct - expect(filters["All Types"]).to.equal(true); - expect(filters["Adjectives"]).to.equal(true); - expect(filters["Nouns"]).to.equal(false); - expect(filters["Verbs"]).to.equal(false); - }); - - it("Should change selection if not available", function() { - // Initialize filters - FilterInput.vocabFilters = [ - {category:"Colors", type:"Verbs"} - ] - - // Get filters - let filters = FilterInput.getCategoryFilters(0); - - // Assert selection changed - expect(filters["All Types"]).to.equal(true); - expect(filters["Adjectives"]).to.equal(true); - expect(filters["Nouns"]).to.equal(false); - expect(filters["Verbs"]).to.equal(false); - expect(FilterInput.vocabFilters[0]["type"]).to.equal("All Types"); - }); - - it("Should not change selection if available", function() { - // Initialize filters - FilterInput.vocabFilters = [ - {category:"Professions", type:"Verbs"} - ] - - // Get filters - let filters = FilterInput.getCategoryFilters(0); - - // Assert selection not changed - expect(filters["All Types"]).to.equal(true); - expect(filters["Adjectives"]).to.equal(false); - expect(filters["Nouns"]).to.equal(true); - expect(filters["Verbs"]).to.equal(true); - expect(FilterInput.vocabFilters[0]["type"]).to.equal("Verbs"); - }); - }); -}); diff --git a/tests/test.filtersPage.js b/tests/test.filtersPage.js @@ -0,0 +1,488 @@ +// filter-input component +describe("FilterInput", function() { + let FilterInput; + beforeEach(function() { + // Create filtersInput component + FilterInput = new filterInput(); + }); + + describe("Created lifecycle hook", function() { + it("Category should be 'verbs'", function() { + expect(FilterInput.category).to.equal("verbs"); + }); + + it("VerbFilters should be empty", function() { + expect(FilterInput.verbFilters.length).to.equal(0); + }); + + it("VocabFilters should be empty", function() { + expect(FilterInput.vocabFilters.length).to.equal(0); + }); + }); + + describe("Value computed property", function() { + it("Should return verb filters if category is 'verbs'", function() { + // Initialize variables + FilterInput.category = "verbs"; + FilterInput.verbFilters = "verb-filters"; + FilterInput.vocabFilters = "vocab-filters"; + + // Assert value returns verb filters + expect(FilterInput.value, "verb-filters"); + }); + + it("Should return vocab filters if category is 'vocab'", function() { + // Initialize variables + FilterInput.category = "vocab"; + FilterInput.verbFilters = "verb-filters"; + FilterInput.vocabFilters = "vocab-filters"; + + // Assert value returns vocab filters + expect(FilterInput.value, "vocab-filters"); + }); + }); + + describe("Value watch", function() { + it("Should emit input event", async function() { + // Initialize variables + FilterInput.category = "verbs"; + FilterInput.verbFilters = ["filter1"]; + + // Override $emit method + let event = ""; + let event_args; + FilterInput.$emit = function(name, value) { + event = name; + event_args = value; + }; + + // Edit verb filters + FilterInput.verbFilters.push("filter2"); + await FilterInput.$nextTick(); + + // Assert event emited + expect(event).to.equal("input"); + expect(event_args).to.have.deep.members(["filter1", "filter2"]); + }); + }); + + describe("AddFilter method", function() { + it("Should add a verb filter if category is 'verbs'", function() { + // Initialize variables + FilterInput.category = "verbs"; + expect(FilterInput.verbFilters.length).to.equal(0); + expect(FilterInput.vocabFilters.length).to.equal(0); + + // Add filter + FilterInput.AddFilter(); + + // Assert filter added + expect(FilterInput.verbFilters).to.have.deep.members([ + {tense:"All Tenses", type:"All Types", subject:"All Subjects", direction:"Eng. → Conj."}, + ]); + expect(FilterInput.vocabFilters).to.have.deep.members([]); + }); + + it("Should add a vocab filter if category is 'vocab'", function() { + // Initialize variables + FilterInput.category = "vocab"; + expect(FilterInput.verbFilters.length).to.equal(0); + expect(FilterInput.vocabFilters.length).to.equal(0); + + // Add filter + FilterInput.AddFilter(); + + // Assert filter added + expect(FilterInput.vocabFilters).to.have.deep.members([ + {category:"All Categories", type:"All Types", direction:"Eng. ↔ Esp."}, + ]); + expect(FilterInput.verbFilters).to.have.deep.members([]); + }); + }); + + describe("RemoveFilter method", function() { + it("Should remove the specified verb filter", function() { + // Initialize filters + FilterInput.category = "verbs"; + FilterInput.verbFilters = [ + "verb1", + "verb2", + "verb3", + ]; + FilterInput.vocabFilters = [ + "vocab1", + "vocab2", + "vocab3", + ]; + + // Remove filter + FilterInput.RemoveFilter(1); + + // Assert filter removed + expect(FilterInput.verbFilters.length).to.equal(2); + expect(FilterInput.verbFilters[0]).to.equal("verb1"); + expect(FilterInput.verbFilters[1]).to.equal("verb3"); + expect(FilterInput.vocabFilters.length).to.equal(3); + }); + + it("Should remove the specified vocab filter", function() { + // Initialize filters + FilterInput.category = "vocab"; + FilterInput.verbFilters = [ + "verb1", + "verb2", + "verb3", + ]; + FilterInput.vocabFilters = [ + "vocab1", + "vocab2", + "vocab3", + ]; + + // Remove filter + FilterInput.RemoveFilter(1); + + // Assert filter removed + expect(FilterInput.verbFilters.length).to.equal(3); + expect(FilterInput.vocabFilters.length).to.equal(2); + expect(FilterInput.vocabFilters[0]).to.equal("vocab1"); + expect(FilterInput.vocabFilters[1]).to.equal("vocab3"); + }); + }); + + describe("GetTenseTypes method", function() { + it("Should be correct for All Tenses", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"All Types", type:"All Types"} + ] + + // Get filters + let filters = FilterInput.getTenseTypes(0); + + // Assert filters are correct + expect(filters["All Types"]).to.equal(true); + expect(filters["Reflexive"]).to.equal(true); + expect(filters["Regular"]).to.equal(true); + expect(filters["Nonregular"]).to.equal(true); + expect(filters["Stem Changing"]).to.equal(true); + expect(filters["Orthographic"]).to.equal(true); + expect(filters["Irregular"]).to.equal(true); + }); + + it("Should be correct for Present Tense", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"Present Tense", type:"All Types"} + ] + + // Get filters + let filters = FilterInput.getTenseTypes(0); + + // Assert filters are correct + expect(filters["All Types"]).to.equal(true); + expect(filters["Reflexive"]).to.equal(true); + expect(filters["Regular"]).to.equal(true); + expect(filters["Nonregular"]).to.equal(true); + expect(filters["Stem Changing"]).to.equal(true); + expect(filters["Orthographic"]).to.equal(false); + expect(filters["Irregular"]).to.equal(true); + }); + + it("Should change selection if not available", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"Present Tense", type:"Orthographic"} + ] + + // Get filters + let filters = FilterInput.getTenseTypes(0); + + // Assert filters are correct + expect(filters["All Types"]).to.equal(true); + expect(filters["Reflexive"]).to.equal(true); + expect(filters["Regular"]).to.equal(true); + expect(filters["Nonregular"]).to.equal(true); + expect(filters["Stem Changing"]).to.equal(true); + expect(filters["Orthographic"]).to.equal(false); + expect(filters["Irregular"]).to.equal(true); + + // Assert selection changed + expect(FilterInput.verbFilters[0]["type"]).to.equal("All Types"); + }); + + it("Should not change selection if available", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"Preterite Tense", type:"Orthographic"} + ] + + // Get filters + let filters = FilterInput.getTenseTypes(0); + + // Assert filters are correct + expect(filters["All Types"]).to.equal(true); + expect(filters["Reflexive"]).to.equal(true); + expect(filters["Regular"]).to.equal(true); + expect(filters["Nonregular"]).to.equal(true); + expect(filters["Stem Changing"]).to.equal(true); + expect(filters["Orthographic"]).to.equal(true); + expect(filters["Irregular"]).to.equal(true); + + // Assert selection not changed + expect(FilterInput.verbFilters[0]["type"]).to.equal("Orthographic"); + }); + }); + + describe("GetTenseSubjects method", function() { + it("Should be correct for All Tenses", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"All Types", type:"All Types"} + ] + + // Get filters + let filters = FilterInput.getTenseSubjects(0); + + // Assert filters are correct + expect(filters["All Subjects"]).to.equal(true); + expect(filters["Yo"]).to.equal(true); + expect(filters["Tú"]).to.equal(true); + expect(filters["Él"]).to.equal(true); + expect(filters["Nosotros"]).to.equal(true); + expect(filters["Ellos"]).to.equal(true); + }); + + it("Should be correct for Present Participles", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"Present Participles", subject:"All Subjects", type:"All Types"} + ] + + // Get filters + let filters = FilterInput.getTenseSubjects(0); + + // Assert filters are correct + expect(filters["All Subjects"]).to.equal(true); + expect(filters["Yo"]).to.equal(false); + expect(filters["Tú"]).to.equal(false); + expect(filters["Él"]).to.equal(false); + expect(filters["Nosotros"]).to.equal(false); + expect(filters["Ellos"]).to.equal(false); + }); + + it("Should change selection if not available", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"Present Participles", subject:"Yo", type:"All Types"} + ] + + // Get filters + let filters = FilterInput.getTenseSubjects(0); + + // Assert filters are correct + expect(filters["All Subjects"]).to.equal(true); + expect(filters["Yo"]).to.equal(false); + expect(filters["Tú"]).to.equal(false); + expect(filters["Él"]).to.equal(false); + expect(filters["Nosotros"]).to.equal(false); + expect(filters["Ellos"]).to.equal(false); + + // Assert selection changed + expect(FilterInput.verbFilters[0]["subject"]).to.equal("All Subjects"); + }); + + it("Should not change selection if available", function() { + // Initialize filters + FilterInput.verbFilters = [ + {tense:"Present Participles", subject:"Type", type:"All Types"}, + {tense:"Preterite Tense", subject:"Yo", type:"All Types"}, + ] + + // Get filters + FilterInput.getTenseSubjects(0); + FilterInput.getTenseSubjects(1); + + // Assert selection not changed + expect(FilterInput.verbFilters[0].subject).to.equal("Type"); + expect(FilterInput.verbFilters[1].subject).to.equal("Yo"); + }); + }); + + describe("GetCategoryFilters method", function() { + it("Should be correct for Verbs", function() { + // Initialize filters + FilterInput.vocabFilters = [ + {category:"Verbs", type:"All Definitions"} + ] + + // Get filters + let filters = FilterInput.getCategoryFilters(0); + + // Assert filters are correct + expect(filters["All Types"]).to.equal(true); + expect(filters["Adjectives"]).to.equal(false); + expect(filters["Nouns"]).to.equal(false); + expect(filters["Verbs"]).to.equal(false); + }); + + it("Should be correct for sets with 1 type", function() { + // Initialize filters + FilterInput.vocabFilters = [ + {category:"Colors", type:"All Definitions"} + ] + + // Get filters + let filters = FilterInput.getCategoryFilters(0); + + // Assert filters are correct + expect(filters["All Types"]).to.equal(true); + expect(filters["Adjectives"]).to.equal(true); + expect(filters["Nouns"]).to.equal(false); + expect(filters["Verbs"]).to.equal(false); + }); + + it("Should change selection if not available", function() { + // Initialize filters + FilterInput.vocabFilters = [ + {category:"Colors", type:"Verbs"} + ] + + // Get filters + let filters = FilterInput.getCategoryFilters(0); + + // Assert selection changed + expect(filters["All Types"]).to.equal(true); + expect(filters["Adjectives"]).to.equal(true); + expect(filters["Nouns"]).to.equal(false); + expect(filters["Verbs"]).to.equal(false); + expect(FilterInput.vocabFilters[0]["type"]).to.equal("All Types"); + }); + + it("Should not change selection if available", function() { + // Initialize filters + FilterInput.vocabFilters = [ + {category:"Professions", type:"Verbs"} + ] + + // Get filters + let filters = FilterInput.getCategoryFilters(0); + + // Assert selection not changed + expect(filters["All Types"]).to.equal(true); + expect(filters["Adjectives"]).to.equal(false); + expect(filters["Nouns"]).to.equal(true); + expect(filters["Verbs"]).to.equal(true); + expect(FilterInput.vocabFilters[0]["type"]).to.equal("Verbs"); + }); + }); +}); + + + +// settings-input component +describe("SettingsInput", function() { + let SettingsInput; + beforeEach(function() { + // Create settingsInput component + SettingsInput = new settingsInput(); + }); + + describe("Value watch", function() { + it("Should emit input event", async function() { + // Override $emit method + let event = ""; + let event_args; + SettingsInput.$emit = function(name, value) { + event = name; + event_args = value; + }; + + // Override setSettings method + let old_setSettings = setSettings; + setSettings = function() {}; + + // Edit setting + SettingsInput.value.promptType = "test-prompt-type"; + await SettingsInput.$nextTick(); + + // Assert event emited + expect(event).to.equal("input"); + expect(event_args.promptType).to.equal("test-prompt-type"); + + // Restore setSettings method + setSettings = old_setSettings; + }); + + it("Should call setSettings", async function() { + // Override $emit method + let event = ""; + let event_args; + SettingsInput.$emit = function(name, value) { + event = name; + event_args = value; + }; + + // Override setSettings method + let old_setSettings = setSettings; + let args = null; + setSettings = function(value) { + args = value; + }; + + // Edit setting + SettingsInput.value.inputType = "test-input-type"; + await SettingsInput.$nextTick(); + // Assert setSettings called + expect(args.inputType).to.equal("test-input-type"); + + // Restore setSettings method + setSettings = old_setSettings; + }); + }); +}); + + +// filters-page component +describe("FiltersPage", function() { + let FiltersPage; + beforeEach(function() { + // Create filtersPage component + FiltersPage = new filtersPage(); + }); + + describe("StartSession method", function() { + it("Should push quizzer page", function() { + // Override $router.push method + let push_args; + FiltersPage.$router = {push: function(args) { + push_args = args; + }}; + + // Initialize variables + FiltersPage.settings = { + promptType: "Text", // Required to prevent browser validation alerts + inputType: "Text", // Required to prevent browser validation alerts + testSetting: "testValue", + }; + + // Call StartSession + FiltersPage.StartSession([1, 2, 3], 0); + + // Assert event emited + expect(push_args).to.deep.equal({ + name: "quizzer", + params: { + startingPrompts: [1, 2, 3], + startingIndex: 0, + settings: { + promptType: "Text", // Required to prevent browser validation alerts + inputType: "Text", // Required to prevent browser validation alerts + testSetting: "testValue", + }, + referer: FiltersPage.category, + } + }); + }); + }); +}); diff --git a/tests/test.global.js b/tests/test.global.js @@ -1,21 +1,4 @@ describe("Global.js", function() { - describe("GetLang method", function() { - it("Should return English by default", function() { - expect(getLang("")).to.equal("en"); - expect(getLang("test")).to.equal("en"); - }); - - it("Should return English for English labels", function() { - expect(getLang("test english test")).to.equal("en"); - expect(getLang("ENGLISH")).to.equal("en"); - }) - - it("Should return Spanish for Spanish labels", function() { - expect(getLang("test spanish test")).to.equal("es"); - expect(getLang("SPANISH")).to.equal("es"); - }) - }); - describe("GetSettings method", function() { it("Settings should be loaded", async function() { // Save original setting from localStorage diff --git a/tests/test.quizzer.js b/tests/test.quizzer.js @@ -42,6 +42,8 @@ describe("Quizzer", function() { describe("Reset method", function() { it("Should reset responce", function() { // Initialize variables + Quizzer.prompts = ["prompt 1", "prompt 2"]; + Quizzer.index = 0; Quizzer.responce = "test"; expect(Quizzer.responce).to.equal("test"); @@ -54,6 +56,8 @@ describe("Quizzer", function() { it("Should set responceActive to true", function() { // Initialize variables + Quizzer.prompts = ["prompt 1", "prompt 2"]; + Quizzer.index = 0; Quizzer.responceActive = false; // Run reset @@ -98,24 +102,6 @@ describe("Quizzer", function() { // Assert event emited expect(event).to.equal("new-prompt"); }); - - it("Should emit 'finished-prompts' event if on last term", function() { - // Initialize variables - Quizzer.prompts = ["prompt 1", "prompt 2"]; - Quizzer.index = 1; - - // Override $emit method - let event = ""; - Quizzer.$emit = function(name) { - event = name; - }; - - // Run reset - Quizzer.Reset(); - - // Assert event emited - expect(event).to.equal("finished-prompts"); - }); }); describe("Submit method", function() { @@ -566,4 +552,21 @@ describe("Quizzer", function() { expect(Quizzer.prompt[3]).to.equal("d2"); }); }); + + describe("GetLang method", function() { + it("Should return English by default", function() { + expect(Quizzer.getLang("")).to.equal("en"); + expect(Quizzer.getLang("test")).to.equal("en"); + }); + + it("Should return English for English labels", function() { + expect(Quizzer.getLang("test english test")).to.equal("en"); + expect(Quizzer.getLang("ENGLISH")).to.equal("en"); + }) + + it("Should return Spanish for Spanish labels", function() { + expect(Quizzer.getLang("test spanish test")).to.equal("es"); + expect(Quizzer.getLang("SPANISH")).to.equal("es"); + }) + }); }); diff --git a/tests/test.settingsInput.js b/tests/test.settingsInput.js @@ -1,60 +0,0 @@ -describe("SettingsInput", function() { - let SettingsInput; - beforeEach(function() { - // Create settingsInput component - SettingsInput = new settingsInput(); - }); - - describe("Value watch", function() { - it("Should emit input event", async function() { - // Override $emit method - let event = ""; - let event_args; - SettingsInput.$emit = function(name, value) { - event = name; - event_args = value; - }; - - // Override setSettings method - let old_setSettings = setSettings; - setSettings = function() {}; - - // Edit setting - SettingsInput.value.promptType = "test-prompt-type"; - await SettingsInput.$nextTick(); - - // Assert event emited - expect(event).to.equal("input"); - expect(event_args.promptType).to.equal("test-prompt-type"); - - // Restore setSettings method - setSettings = old_setSettings; - }); - - it("Should call setSettings", async function() { - // Override $emit method - let event = ""; - let event_args; - SettingsInput.$emit = function(name, value) { - event = name; - event_args = value; - }; - - // Override setSettings method - let old_setSettings = setSettings; - let args = null; - setSettings = function(value) { - args = value; - }; - - // Edit setting - SettingsInput.value.inputType = "test-input-type"; - await SettingsInput.$nextTick(); - // Assert setSettings called - expect(args.inputType).to.equal("test-input-type"); - - // Restore setSettings method - setSettings = old_setSettings; - }); - }); -});