diff --git a/client/stylesheets/animations.scss b/client/stylesheets/animations.scss index e614a3b..c873fea 100644 --- a/client/stylesheets/animations.scss +++ b/client/stylesheets/animations.scss @@ -1,6 +1,16 @@ +@import "utilities/animation_key_frames"; + .animate-flicker { -webkit-animation: flickerAnimation 3s infinite; -moz-animation: flickerAnimation 3s infinite; -o-animation: flickerAnimation 3s infinite; animation: flickerAnimation 3s infinite; } + +.animate-shake-wrong { + color : red; + animation: shakeWrongAnimation 0.4s ease 0s 2 alternate; + -o-animation: shakeWrongAnimation 0.4s ease 0s 2 alternate; + -moz-animation: shakeWrongAnimation 0.4s ease 0s 2 alternate; + -webkit-animation: shakeWrongAnimation 0.4s ease 0s 2 alternate; +} \ No newline at end of file diff --git a/client/stylesheets/challenge.scss b/client/stylesheets/challenge.scss index 5dbfb96..435d9d3 100644 --- a/client/stylesheets/challenge.scss +++ b/client/stylesheets/challenge.scss @@ -172,6 +172,17 @@ color: #bbbbbb; margin-right: 10px; } + +/* Replace Wrong Word Choices*/ +.phraseWord.phraseWordClickable { + &:hover { + background-color: $very-light-gray; + } +} + +.phraseWord.wrong { + color: $light-green; +} /* Report area */ #feedback-area { diff --git a/client/stylesheets/utilities/_animation_key_frames.scss b/client/stylesheets/utilities/_animation_key_frames.scss index 817dc02..1252b78 100644 --- a/client/stylesheets/utilities/_animation_key_frames.scss +++ b/client/stylesheets/utilities/_animation_key_frames.scss @@ -1,5 +1,65 @@ /* Animations */ +@keyframes shakeWrongAnimation { + 0% { + transform: rotate(0deg); + } + 33% { + transform: rotate(7deg); + } + 66% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-7deg); + } +} + +@-o-keyframes shakeWrongAnimation { + 0% { + transform: rotate(0deg); + } + 33% { + transform: rotate(7deg); + } + 66% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-7deg); + } +} + +@-moz-keyframes shakeWrongAnimation { + 0% { + transform: rotate(0deg); + } + 33% { + transform: rotate(7deg); + } + 66% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-7deg); + } +} + +@-webkit-keyframes shakeWrongAnimation { + 0% { + transform: rotate(0deg); + } + 33% { + transform: rotate(7deg); + } + 66% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-7deg); + } +} + @keyframes flickerAnimation { 0% { opacity: 1; @@ -47,3 +107,5 @@ opacity: 1; } } + + diff --git a/client/template/challenge/challenge.js b/client/template/challenge/challenge.js index 09955cb..536a2bc 100644 --- a/client/template/challenge/challenge.js +++ b/client/template/challenge/challenge.js @@ -43,12 +43,19 @@ Template.challenge.helpers({ return "Match with correct answer"; case QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC: return "Match with correct picture"; + case QTYPE.MULTIPLE_CHOICES_MULTIPLE_ANSWERS: + return "Pick all answers that mean the same thing"; case QTYPE.TRUE_FALSE: return "True or False"; case QTYPE.WORD_PAIRING: return "Match the pairs"; case QTYPE.REARRANGE: return "Arrange this phrase in Vietnamese"; + case QTYPE.FILL_IN_BLANK: + return "Which word goes into the blank?" + case QTYPE.REPLACE_WRONG_WORD: + return "One of the words in the Vietnamese phrase is wrong. Choose which and its replacement." + } }, completed: function() { @@ -174,7 +181,10 @@ function setupQuestion(lesson, qType) { case QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC: setupMultipleChoicesTranslation(lesson); break; - + case QTYPE.MULTIPLE_CHOICES_MULTIPLE_ANSWERS: + setupMultipleChoicesMultipleAnswers(lesson); + break; + case QTYPE.WORD_PAIRING: setupWordPairing(lesson); break; @@ -182,7 +192,12 @@ function setupQuestion(lesson, qType) { case QTYPE.REARRANGE: setupRearrange(lesson); break; - + case QTYPE.FILL_IN_BLANK: + setupFillInBlank(lesson); + break; + case QTYPE.REPLACE_WRONG_WORD: + setupReplaceWrongWord(lesson); + break; default: return; } @@ -255,7 +270,7 @@ function challengeComplete(lesson) { var nextLevel = user.profile.level + 1; if (user.profile.xp >= LEVEL_XP_REQUIREMENTS[nextLevel]) { Session.set("reachedNewLevel", true); - Meteor.call('unlockLevel', userId, nextLevel); + Meteor.call("unlockLevel", userId, nextLevel); } } @@ -283,11 +298,20 @@ answer = function(lesson) { case QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC: answerScore = aMultipleChoicesTranslation(phrase); break; + case QTYPE.MULTIPLE_CHOICES_MULTIPLE_ANSWERS: + answerScore = aMultipleChoicesMultipleAnswers(phrase); + break; case QTYPE.WORD_PAIRING: answerScore = aWordPairing(); case QTYPE.REARRANGE: answerScore = aRearrange(); break; + case QTYPE.FILL_IN_BLANK: + answerScore = aFillInBlank(); + break; + case QTYPE.REPLACE_WRONG_WORD: + answerScore = aReplaceWrongWord(); + break; } computeProgress(answerScore); diff --git a/client/template/challenge/question_area.js b/client/template/challenge/question_area.js index f90f00c..7d28435 100644 --- a/client/template/challenge/question_area.js +++ b/client/template/challenge/question_area.js @@ -11,10 +11,16 @@ Template.questionArea.helpers({ case QTYPE.MULTIPLE_CHOICES_TRANSLATION: case QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC: return "qMultipleChoicesTranslation"; + case QTYPE.MULTIPLE_CHOICES_MULTIPLE_ANSWERS: + return "qMultipleChoicesMultipleAnswers"; case QTYPE.WORD_PAIRING: return "qWordPairing"; case QTYPE.REARRANGE: return "qRearrange"; + case QTYPE.FILL_IN_BLANK: + return "qFillInBlank"; + case QTYPE.REPLACE_WRONG_WORD: + return "qReplaceWrongWord"; default: return; } diff --git a/client/template/challenge/types/q_fill_in_blank.html b/client/template/challenge/types/q_fill_in_blank.html new file mode 100644 index 0000000..3ff1d8a --- /dev/null +++ b/client/template/challenge/types/q_fill_in_blank.html @@ -0,0 +1,12 @@ + diff --git a/client/template/challenge/types/q_fill_in_blank.js b/client/template/challenge/types/q_fill_in_blank.js new file mode 100644 index 0000000..ec02ad5 --- /dev/null +++ b/client/template/challenge/types/q_fill_in_blank.js @@ -0,0 +1,125 @@ +const CHOICE_NUM = 4; + +Template.qFillInBlank.helpers({ + + phrase: function() { + var phrase = this.phrases[Session.get("phraseIndex")]; + var prompt = phrase.vnPhraseLower + " _____ " + phrase.vnPhraseUpper; + return prompt; + }, + + choices: function() { + return Session.get("choices"); + }, + gotFeedback: function() { + return Session.equals("qState", QSTATE.CONTINUE); + }, +}); + +Template.qFillInBlank.events({ + + "click .choice": function(ev) { + + // Select a choice + var choices = Session.get("choices"); + var expectedAnswers = Session.get("expectedAnswers"); + var clickWord = ev.target.id; + + var clickChoice = _.findWhere(choices, {displayedWord: clickWord}); + var checkedChoices = _.where(choices, { + checked: true + }); + + var ONLY_ALLOW_ONE_ANSWER = true; // placeholder for generic multiple choice template work + + if(checkedChoices.length && ONLY_ALLOW_ONE_ANSWER) { + // if clicked on a checked choice, then uncheck it (outside of this block) + // otherwise uncheck the previously checked choice + if(clickChoice.displayedWord !== checkedChoices[0].displayedWord) { + checkedChoices[0].checked = false; + } + + } + clickChoice.checked = !clickChoice.checked; + + // Update session + Session.set("choices", choices); + + // Allow for answering + enableSubmitButton(); + } + +}); + +// ---------------------- +// Public functions +// ---------------------- + +setupFillInBlank = function(lesson) { + pickChoices(lesson); +} + +aFillInBlank = function(phrase) { + var checkedChoices = _.where(Session.get("choices"), { + checked: true + }); + checkedChoices = _.map(checkedChoices, function(answer) { + return answer.displayedWord; + }); + + var expectedAnswers = Session.get("expectedAnswers"); + + + var unionCheckExpect = _.union(checkedChoices, expectedAnswers); + + var answerCorrect = false; + var feedback = "no feedback"; + if(expectedAnswers.length === checkedChoices.length && + unionCheckExpect.length === checkedChoices.length) { + // user checked only the correct answers + answerCorrect = true + } else { + feedback = s.toSentence(expectedAnswers); + } + + Session.set("feedback", feedback); + + return answerCorrect? CHALLENGE_PROGRESS_CORRECT : + CHALLENGE_PROGRESS_WRONG; +} + +// ---------------------- +// Private functions +// ---------------------- + +function pickChoices(lesson) { + var phraseIndex = Session.get("phraseIndex"); + var phrase = lesson.phrases[phraseIndex]; + + var expectedAnswers = new Array(); + expectedAnswers.push(phrase.answer); + + var choices = phrase.wrongChoices; + + + choices = _.map(choices, function(choice) { + return { + checked: false, + isCorrect: false, + displayedWord: choice, + }; + }); + + // push answer object onto the choices + choices.push( { + checked: false, + isCorrect: true, + displayedWord: phrase.answer, + }); + + choices = _.shuffle(choices); + + Session.set("choices", choices); + Session.set("expectedAnswers", expectedAnswers); + +} diff --git a/client/template/challenge/types/q_multiple_choices_multiple_answers.html b/client/template/challenge/types/q_multiple_choices_multiple_answers.html new file mode 100644 index 0000000..c1d10d2 --- /dev/null +++ b/client/template/challenge/types/q_multiple_choices_multiple_answers.html @@ -0,0 +1,12 @@ + diff --git a/client/template/challenge/types/q_multiple_choices_multiple_answers.js b/client/template/challenge/types/q_multiple_choices_multiple_answers.js new file mode 100644 index 0000000..6885a0d --- /dev/null +++ b/client/template/challenge/types/q_multiple_choices_multiple_answers.js @@ -0,0 +1,124 @@ +const CHOICE_NUM = 4; + +Template.qMultipleChoicesMultipleAnswers.helpers({ + + phrase: function() { + var phrase = this.phrases[Session.get("phraseIndex")]; + return _.first(phrase.english); + }, + + choices: function() { + return Session.get("choices"); + }, + gotFeedback: function() { + return Session.equals("qState", QSTATE.CONTINUE); + }, +}); + +Template.qMultipleChoicesMultipleAnswers.events({ + + "click .choice": function(ev) { + + // Select a choice + var choices = Session.get("choices"); + var expectedAnswers = Session.get("expectedAnswers"); + var clickWord = ev.target.id; + + var clickChoice = _.findWhere(choices, {displayedWord: clickWord}); + + clickChoice.checked = !clickChoice.checked; + if(_.indexOf(expectedAnswers,clickChoice.displayedWord) !== -1) { + clickChoice.isCorrect = true; + } + + // Update session + Session.set("choices", choices); + + // Allow for answering + enableSubmitButton(); + } + +}); + +// ---------------------- +// Public functions +// ---------------------- + +setupMultipleChoicesMultipleAnswers = function(lesson) { + pickChoices(lesson); +} + +aMultipleChoicesMultipleAnswers = function(phrase) { + var checkedAnswers = _.where(Session.get("choices"), { + checked: true + }); + checkedAnswers = _.map(checkedAnswers, function(answer) { + return answer.displayedWord; + }); + + var expectedAnswers = Session.get("expectedAnswers"); + + + var unionCheckExpect = _.union(checkedAnswers, expectedAnswers); + + var answerCorrect = false; + var feedback = "no feedback"; + if(expectedAnswers.length === checkedAnswers.length && + unionCheckExpect.length === checkedAnswers.length) { + // user checked only the correct answers + answerCorrect = true + } else { + feedback = s.toSentence(expectedAnswers); + } + + Session.set("feedback", feedback); + + return answerCorrect? CHALLENGE_PROGRESS_CORRECT : + CHALLENGE_PROGRESS_WRONG; +} + +// ---------------------- +// Private functions +// ---------------------- + +function pickChoices(lesson) { + var phraseIndex = Session.get("phraseIndex"); + var phrase = lesson.phrases[phraseIndex]; + var vnPhrases = phrase.vietnamese; + var choices = lesson.phrases; + var expectedAnswers = phrase.vietnamese; + + + vnPhrases = _.map(vnPhrases, function(phrase) { + return { + vietnamese: phrase + }; + }); + // Reject other choices that are not MCT(P) or contain vn phrases of current question + choices = _.filter(choices, function(choice) { + var choiceNotMCT = choice.qType === QTYPE.MULTIPLE_CHOICES_TRANSLATION; + var choiceNotMCTP = choice.qType === QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC; + var choiceVnNotInSolutions = !_.contains(expectedAnswers, choice.vietnamese); + return (choiceNotMCT || choiceNotMCTP) && choiceVnNotInSolutions; + }); + + // Minus one choice since we will re-add the correct choice later + var choices = _.sample(choices, CHOICE_NUM - expectedAnswers.length); + + choices = choices.concat(vnPhrases); + // Randomize! + choices = _.shuffle(choices); + + // Add hotkey and other properties + _.map(choices, function(choice) { + return _.extend(choice, { + checked: false, + isCorrect: false, + displayedWord: choice.vietnamese, + }); + }); + + Session.set("choices", choices); + Session.set("expectedAnswers", expectedAnswers); + +} diff --git a/client/template/challenge/types/q_replace_wrong_word.html b/client/template/challenge/types/q_replace_wrong_word.html new file mode 100644 index 0000000..1f32bb1 --- /dev/null +++ b/client/template/challenge/types/q_replace_wrong_word.html @@ -0,0 +1,26 @@ + diff --git a/client/template/challenge/types/q_replace_wrong_word.js b/client/template/challenge/types/q_replace_wrong_word.js new file mode 100644 index 0000000..c829424 --- /dev/null +++ b/client/template/challenge/types/q_replace_wrong_word.js @@ -0,0 +1,247 @@ +const CHOICE_NUM = 4; + +Template.qReplaceWrongWord.helpers({ + + phraseWords: function() { + return Session.get("phraseWords"); + }, + + choices: function() { + return Session.get("choices"); + }, + gotFeedback: function() { + return Session.equals("qState", QSTATE.CONTINUE); + }, + englishTranslation: function() { + return Session.get("englishTranslation"); + }, + replaceEnabled: function() { // toggles clickability of words in phrase + return getReplaceEnabled(); + }, + showMultipleChoice: function() { + return !getReplaceEnabled(); + }, + // animationEnabled: function (){ + // return Session.get("animationEnabled"); + // } +}); + + +Template.qReplaceWrongWord.events({ + +// @TODO -- generic MC template base + "click .choice": function(ev) { + + // Select a choice + var choices = Session.get("choices"); + var expectedAnswers = Session.get("expectedAnswers"); + var clickWord = ev.target.id; + + var clickChoice = _.findWhere(choices, {displayedWord: clickWord}); + var checkedChoices = _.where(choices, { + checked: true + }); + + var ONLY_ALLOW_ONE_ANSWER = true; // placeholder for generic multiple choice template work + + if(checkedChoices.length && ONLY_ALLOW_ONE_ANSWER) { + // if clicked on a checked choice, then uncheck it (outside of this block) + // otherwise uncheck the previously checked choice + if(clickChoice.displayedWord !== checkedChoices[0].displayedWord) { + checkedChoices[0].checked = false; + } + + } + clickChoice.checked = !clickChoice.checked; + + // Update session + Session.set("choices", choices); + + // Allow for answering + enableSubmitButton(); + }, + + + "click .phraseWordClickable": function (ev) { + var phraseWords = Session.get("phraseWords"); + var clickWord = ev.target.textContent; + var selectedWord = _.findWhere(phraseWords, {word: clickWord}); + + if (selectedWord.isWrong) { // user selected the word that needs to be replaced + // show multiple choice selection + Session.set("replaceEnabled", false); + } else { // user selected a word that doesn't need to be replaced + + if(!Session.get("pointDeduction")) { // halve possible score for question + Session.set("pointDeduction", CHALLENGE_PROGRESS_CORRECT/2); + } + + selectedWord.animationEnabled = true; + Session.set("phraseWords", phraseWords); + } + + }, + "animationend .phraseWord" : disableAnimation, + "oAnimationEnd .phraseWord" : disableAnimation, + "webkitAnimationEnd .phraseWord" : disableAnimation, + +}); + + +// ---------------------- +// Public functions +// ---------------------- + +setupReplaceWrongWord = function(lesson) { + pickChoices(lesson); +} + +aReplaceWrongWord = function(phrase) { + var answerScore = aMultipleChoice(phrase); + var pointDeduction = Session.get("pointDeduction"); + +// deduct points for not selecting wrong word in phrase on first try + if (answerScore == CHALLENGE_PROGRESS_CORRECT) { + answerScore -= pointDeduction; + + } + return answerScore; +} + +// ---------------------- +// Private functions +// ---------------------- + +function disableAnimation(ev) { + var phraseWords = Session.get("phraseWords"); + var clickWord = ev.target.textContent; + var selectedWord = _.findWhere(phraseWords, {word: clickWord}); + + selectedWord.animationEnabled = false; + + Session.set("phraseWords", phraseWords); +} + +// @TODO -- generic MC template base +// check if multiple choice answer is correct +function aMultipleChoice(phrase) { + var checkedChoices = _.where(Session.get("choices"), { + checked: true + }); + checkedChoices = _.map(checkedChoices, function(answer) { + return answer.displayedWord; + }); + + var expectedAnswers = Session.get("expectedAnswers"); + +// @TODO -- look into using isCorrect fields to make below more readable + var unionCheckExpect = _.union(checkedChoices, expectedAnswers); + + var answerCorrect = false; + var feedback = "no feedback"; + if(expectedAnswers.length === checkedChoices.length && + unionCheckExpect.length === checkedChoices.length) { + // user checked only the correct answers + answerCorrect = true + } else { + feedback = s.toSentence(expectedAnswers); + } + + Session.set("feedback", feedback); + + return answerCorrect? CHALLENGE_PROGRESS_CORRECT : + CHALLENGE_PROGRESS_WRONG; +} + +function pickChoices(lesson) { + var phraseIndex = Session.get("phraseIndex"); + var phrase = lesson.phrases[phraseIndex]; + + // base this RWW question on random existing FIB question + var possibleQuestions = _.where(lesson.phrases, {qType:QTYPE.FILL_IN_BLANK}); + var question = _.sample(possibleQuestions, 1).pop(); + +// setup question data + setupPhraseWords(question); + + var wrongAnswers = question.wrongChoices; + var rightAnswers = new Array(); + rightAnswers.push(question.answer); + + setupMultipleChoice(wrongAnswers, rightAnswers); + + + Session.set("replaceEnabled", true); + // Session.set("animationEnabled", false); + + Session.set("pointDeduction", 0); // used for wrong selection of incorrect word +} + +// helper +function toChoiceObj (correct, word) { + return { + displayedWord: word, + isCorrect: correct, + checked: false + }; +} + +// @TODO -- generic MC template base +// both inputs are lists of words/phrases +function setupMultipleChoice(wrongAnswers, rightAnswers) { + // + Session.set("expectedAnswers", rightAnswers); + + // setup choice object creation functions + var toRightChoice = _.partial(toChoiceObj, true); + var toWrongChoice = _.partial(toChoiceObj, false); + +// setup multiple choices: + wrongAnswers = _.sample(wrongAnswers, CHOICE_NUM - rightAnswers.length); + + rightAnswers = _.map(rightAnswers, toRightChoice); + wrongAnswers = _.map(wrongAnswers, toWrongChoice); + + + var choices = wrongAnswers.concat(rightAnswers); + choices = _.shuffle(choices); + + Session.set("choices", choices); + +} + +// helper +function toWordObj (wrongVal, choice) { + return { + word: choice, + isWrong: wrongVal, + animationEnabled: false + }; +}; + +// used to create displayed phraseWords objects +function setupPhraseWords(question) { + + var toWrongWordObj = _.partial(toWordObj, true); + var toCorrectWordObj = _.partial(toWordObj, false); + + var wrongWord = toWrongWordObj( _.sample(question.wrongChoices, 1).pop()); + + // add attributes to phrasewords + var phraseWords = s.words(question.vnPhraseLower); + + phraseWords = _.map(phraseWords, toCorrectWordObj); + + // put all word objects of phrase into a single array + phraseWords.push(wrongWord); + phraseWords =phraseWords.concat(_.map(s.words(question.vnPhraseUpper), toCorrectWordObj)); + Session.set("phraseWords", phraseWords); + + var englishTranslation = question.english[0]; + Session.set("englishTranslation", englishTranslation); +} + +// template helper helper +function getReplaceEnabled(){ + return Session.get("replaceEnabled"); +} \ No newline at end of file diff --git a/client/template/challenge/types/q_word_pairing.js b/client/template/challenge/types/q_word_pairing.js index 7a936c8..372700c 100644 --- a/client/template/challenge/types/q_word_pairing.js +++ b/client/template/challenge/types/q_word_pairing.js @@ -47,7 +47,6 @@ function compareChoices(prevChoiceWord, curChoiceWord) { if (prevChoice.matchingWord === curChoice.displayedWord) { curChoice.checked = true; Session.set("numMatches", Session.get("numMatches") + 1); - // TODO: disable both buttons if(Session.get("numMatches") === CHOICE_NUM) { enableSubmitButton(); } diff --git a/server/lessons/BASIC_1.js b/server/lessons/BASIC_1.js index 02c19a5..0fed84e 100644 --- a/server/lessons/BASIC_1.js +++ b/server/lessons/BASIC_1.js @@ -7,9 +7,22 @@ BASIC_1 = { content: ["basic1"], phrases: [ { + qType: QTYPE.REPLACE_WRONG_WORD, + }, { + qType: QTYPE.FILL_IN_BLANK, + vnPhraseLower: "Tôi", + vnPhraseUpper: "phụ nữ.", + answer: "là", + wrongChoices: ["la", "lá", "lã"], + english: ["I am a woman", "I'm a woman"] + }, { + qType: QTYPE.MULTIPLE_CHOICES_MULTIPLE_ANSWERS, + vietnamese: ["Đàn ông", "Đàn ông2!!!"], + english: ["Man"] + }, { qType: QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC, - image: "/img/lessons/man.jpg", vietnamese: "Đàn ông", + image: "/img/lessons/man.jpg", english: ["Man"] }, { qType: QTYPE.MULTIPLE_CHOICES_TRANSLATION_PIC,