');
}
// Setup questions
var quiz = $(''),
count = 1;
// Loop through questions object
for (i in questions) {
if (questions.hasOwnProperty(i)) {
var question = questions[i];
var questionHTML = $('');
if (plugin.config.displayQuestionCount) {
questionHTML.append('
');
// Count the number of true values
var truths = 0;
for (i in question.a) {
if (question.a.hasOwnProperty(i)) {
answer = question.a[i];
if (answer.correct) {
truths++;
}
}
}
// Now let's append the answers with checkboxes or radios depending on truth count
var answerHTML = $('
');
// Get the answers
var answers = plugin.config.randomSortAnswers ?
question.a.sort(function() { return (Math.round(Math.random())-0.5); }) :
question.a;
// prepare a name for the answer inputs based on the question
var selectAny = question.select_any ? question.select_any : false,
forceCheckbox = question.force_checkbox ? question.force_checkbox : false,
checkbox = (truths > 1 && !selectAny) || forceCheckbox,
inputName = $element.attr('id') + '_question' + (count - 1),
inputType = checkbox ? 'checkbox' : 'radio';
if( count == quizValues.questions.length ) {
nextQuestionClass = nextQuestionClass + ' ' + lastQuestionClass;
}
for (i in answers) {
if (answers.hasOwnProperty(i)) {
answer = answers[i],
optionId = inputName + '_' + i.toString();
// If question has >1 true answers and is not a select any, use checkboxes; otherwise, radios
var input = ' ';
var optionLabel = '';
var answerContent = $('')
.append(input)
.append(optionLabel);
answerHTML.append(answerContent);
}
}
// Append answers to question
questionHTML.append(answerHTML);
// If response messaging is NOT disabled, add it
if (plugin.config.perQuestionResponseMessaging || plugin.config.completionResponseMessaging) {
// Now let's append the correct / incorrect response messages
var responseHTML = $('
');
responseHTML.append('
' + question.correct + '
');
responseHTML.append('
' + question.incorrect + '
');
// Append responses to question
questionHTML.append(responseHTML);
}
// Appends check answer / back / next question buttons
if (plugin.config.backButtonText && plugin.config.backButtonText !== '') {
questionHTML.append('' + plugin.config.backButtonText + '');
}
var nextText = plugin.config.nextQuestionText;
if (plugin.config.completeQuizText && count == questionCount) {
nextText = plugin.config.completeQuizText;
}
// If we're not showing responses per question, show next question button and make it check the answer too
if (!plugin.config.perQuestionResponseMessaging) {
questionHTML.append('' + nextText + '');
} else {
questionHTML.append('' + nextText + '');
questionHTML.append('' + plugin.config.checkAnswerText + '');
}
// Append question & answers to quiz
quiz.append(questionHTML);
count++;
}
}
// Add the quiz content to the page
$quizArea.append(quiz);
// Toggle the start button OR start the quiz if start button is disabled
if (plugin.config.skipStartButton || $quizStarter.length == 0) {
$quizStarter.hide();
plugin.method.startQuiz.apply (this, [{callback: plugin.config.animationCallbacks.startQuiz}]); // TODO: determine why 'this' is being passed as arg to startQuiz method
kN(key,3).apply (null, []);
} else {
$quizStarter.fadeIn(500, kN(key,3)); // 3d notch on key must be on both sides of if/else, otherwise key won't turn
}
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
},
// Starts the quiz (hides start button and displays first question)
startQuiz: function(options) {
var key, keyNotch, kN;
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
kN = keyNotch; // you specify the notch, you get a callback function for your animation
function start(options) {
var firstQuestion = $(_element + ' ' + _questions + ' li').first();
if (firstQuestion.length) {
firstQuestion.fadeIn(500, function () {
if (options && options.callback) options.callback ();
});
}
}
if (plugin.config.skipStartButton || $quizStarter.length == 0) {
start({callback: kN(key,1)});
} else {
$quizStarter.fadeOut(300, function(){
start({callback: kN(key,1)}); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
});
}
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
if (plugin.config.events &&
plugin.config.events.onStartQuiz) {
plugin.config.events.onStartQuiz.apply (null, []);
}
},
// Resets (restarts) the quiz (hides results, resets inputs, and displays first question)
resetQuiz: function(startButton, options) {
var key, keyNotch, kN;
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
kN = keyNotch; // you specify the notch, you get a callback function for your animation
$quizResults.fadeOut(300, function() {
$(_element + ' input').prop('checked', false).prop('disabled', false);
$quizLevel.attr('class', 'quizLevel');
$(_element + ' ' + _question).removeClass(correctClass).removeClass(incorrectClass).remove(completeClass);
$(_element + ' ' + _answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass);
$(_element + ' ' + _question + ',' +
_element + ' ' + _responses + ',' +
_element + ' ' + _response + ',' +
_element + ' ' + _nextQuestionBtn + ',' +
_element + ' ' + _prevQuestionBtn
).hide();
$(_element + ' ' + _questionCount + ',' +
_element + ' ' + _answers + ',' +
_element + ' ' + _checkAnswerBtn
).show();
$quizArea.append($(_element + ' ' + _questions)).show();
kN(key,1).apply (null, []);
plugin.method.startQuiz({callback: plugin.config.animationCallbacks.startQuiz},$quizResults); // TODO: determine why $quizResults is being passed
});
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
},
// Validates the response selection(s), displays explanations & next question button
checkAnswer: function(checkButton, options) {
var key, keyNotch, kN;
key = internal.method.getKey (2); // how many notches == how many jQ animations you will run
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
kN = keyNotch; // you specify the notch, you get a callback function for your animation
var questionLI = $($(checkButton).parents(_question)[0]),
answerLIs = questionLI.find(_answers + ' li'),
answerSelects = answerLIs.find('input:checked'),
questionIndex = parseInt(questionLI.attr('id').replace(/(question)/, ''), 10),
answers = questions[questionIndex].a,
selectAny = questions[questionIndex].select_any ? questions[questionIndex].select_any : false;
answerLIs.addClass(incorrectResponseClass);
// Collect the true answers needed for a correct response
var trueAnswers = [];
for (i in answers) {
if (answers.hasOwnProperty(i)) {
var answer = answers[i],
index = parseInt(i, 10);
if (answer.correct) {
trueAnswers.push(index);
answerLIs.eq(index).removeClass(incorrectResponseClass).addClass(correctResponseClass);
}
}
}
// TODO: Now that we're marking answer LIs as correct / incorrect, we might be able
// to do all our answer checking at the same time
// NOTE: Collecting answer index for comparison aims to ensure that HTML entities
// and HTML elements that may be modified by the browser / other scrips match up
// Collect the answers submitted
var selectedAnswers = [];
answerSelects.each( function() {
var id = $(this).attr('id');
selectedAnswers.push(parseInt(id.replace(/(.*\_question\d{1,}_)/, ''), 10));
});
if (plugin.config.preventUnanswered && selectedAnswers.length === 0) {
alert(plugin.config.preventUnansweredText);
return false;
}
// Verify all/any true answers (and no false ones) were submitted
var correctResponse = plugin.method.compareAnswers(trueAnswers, selectedAnswers, selectAny);
if (correctResponse) {
questionLI.addClass(correctClass);
} else {
questionLI.addClass(incorrectClass);
}
// Toggle appropriate response (either for display now and / or on completion)
questionLI.find(correctResponse ? _correctResponse : _incorrectResponse).show();
// If perQuestionResponseMessaging is enabled, toggle response and navigation now
if (plugin.config.perQuestionResponseMessaging) {
$(checkButton).hide();
if (!plugin.config.perQuestionResponseAnswers) {
// Make sure answers don't highlight for a split second before they hide
questionLI.find(_answers).hide({
duration: 0,
complete: function() {
questionLI.addClass(completeClass);
}
});
} else {
questionLI.addClass(completeClass);
}
questionLI.find('input').prop('disabled', true);
questionLI.find(_responses).show();
questionLI.find(_nextQuestionBtn).fadeIn(300, kN(key,1));
questionLI.find(_prevQuestionBtn).fadeIn(300, kN(key,2));
if (!questionLI.find(_prevQuestionBtn).length) kN(key,2).apply (null, []); // 2nd notch on key must be passed even if there's no "back" button
} else {
kN(key,1).apply (null, []); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
kN(key,2).apply (null, []); // 2nd notch on key must be on both sides of if/else, otherwise key won't turn
}
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
},
// Moves to the next question OR completes the quiz if on last question
nextQuestion: function(nextButton, options) {
var key, keyNotch, kN;
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
kN = keyNotch; // you specify the notch, you get a callback function for your animation
var currentQuestion = $($(nextButton).parents(_question)[0]),
nextQuestion = currentQuestion.next(_question),
answerInputs = currentQuestion.find('input:checked');
// If response messaging has been disabled or moved to completion,
// make sure we have an answer if we require it, let checkAnswer handle the alert messaging
if (plugin.config.preventUnanswered && answerInputs.length === 0) {
return false;
}
if (nextQuestion.length) {
currentQuestion.fadeOut(300, function(){
nextQuestion.find(_prevQuestionBtn).show().end().fadeIn(500, kN(key,1));
if (!nextQuestion.find(_prevQuestionBtn).show().end().length) kN(key,1).apply (null, []); // 1st notch on key must be passed even if there's no "back" button
});
} else {
kN(key,1).apply (null, []); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
plugin.method.completeQuiz({callback: plugin.config.animationCallbacks.completeQuiz});
}
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
},
// Go back to the last question
backToQuestion: function(backButton, options) {
var key, keyNotch, kN;
key = internal.method.getKey (2); // how many notches == how many jQ animations you will run
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
kN = keyNotch; // you specify the notch, you get a callback function for your animation
var questionLI = $($(backButton).parents(_question)[0]),
responses = questionLI.find(_responses);
// Back to question from responses
if (responses.css('display') === 'block' ) {
questionLI.find(_responses).fadeOut(300, function(){
questionLI.removeClass(correctClass).removeClass(incorrectClass).removeClass(completeClass);
questionLI.find(_responses + ', ' + _response).hide();
questionLI.find(_answers).show();
questionLI.find(_answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass);
questionLI.find('input').prop('disabled', false);
questionLI.find(_answers).fadeIn(500, kN(key,1)); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
questionLI.find(_checkAnswerBtn).fadeIn(500, kN(key,2));
questionLI.find(_nextQuestionBtn).hide();
// if question is first, don't show back button on question
if (questionLI.attr('id') != 'question0') {
questionLI.find(_prevQuestionBtn).show();
} else {
questionLI.find(_prevQuestionBtn).hide();
}
});
// Back to previous question
} else {
var prevQuestion = questionLI.prev(_question);
questionLI.fadeOut(300, function() {
prevQuestion.removeClass(correctClass).removeClass(incorrectClass).removeClass(completeClass);
prevQuestion.find(_responses + ', ' + _response).hide();
prevQuestion.find(_answers).show();
prevQuestion.find(_answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass);
prevQuestion.find('input').prop('disabled', false);
prevQuestion.find(_nextQuestionBtn).hide();
prevQuestion.find(_checkAnswerBtn).show();
if (prevQuestion.attr('id') != 'question0') {
prevQuestion.find(_prevQuestionBtn).show();
} else {
prevQuestion.find(_prevQuestionBtn).hide();
}
prevQuestion.fadeIn(500, kN(key,1));
kN(key,2).apply (null, []); // 2nd notch on key must be on both sides of if/else, otherwise key won't turn
});
}
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
},
// Hides all questions, displays the final score and some conclusive information
completeQuiz: function(options) {
var key, keyNotch, kN;
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
kN = keyNotch; // you specify the notch, you get a callback function for your animation
var score = $(_element + ' ' + _correct).length,
displayScore = score;
if (plugin.config.scoreAsPercentage) {
displayScore = (score / questionCount).toFixed(2)*100 + "%";
}
if (plugin.config.disableScore) {
$(_quizScore).remove()
} else {
$(_quizScore + ' span').html(plugin.config.scoreTemplateText
.replace('%score', displayScore).replace('%total', questionCount));
}
if (plugin.config.disableRanking) {
$(_quizLevel).remove()
} else {
var levels = [
quizValues.info.level1, // 80-100%
quizValues.info.level2, // 60-79%
quizValues.info.level3, // 40-59%
quizValues.info.level4, // 20-39%
quizValues.info.level5 // 0-19%
],
levelRank = plugin.method.calculateLevel(score),
levelText = $.isNumeric(levelRank) ? levels[levelRank] : '';
$(_quizLevel + ' span').html(levelText);
$(_quizLevel).addClass('level' + levelRank);
}
$quizArea.fadeOut(300, function() {
// If response messaging is set to show upon quiz completion, show it now
if (plugin.config.completionResponseMessaging) {
$(_element + ' .button:not(' + _tryAgainBtn + '), ' + _element + ' ' + _questionCount).hide();
$(_element + ' ' + _question + ', ' + _element + ' ' + _answers + ', ' + _element + ' ' + _responses).show();
$quizResults.append($(_element + ' ' + _questions)).fadeIn(500, kN(key,1));
} else {
$quizResults.fadeIn(500, kN(key,1)); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
}
});
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
if (plugin.config.events &&
plugin.config.events.onCompleteQuiz) {
plugin.config.events.onCompleteQuiz.apply (null, [{
questionCount: questionCount,
score: score
}]);
}
},
// Compares selected responses with true answers, returns true if they match exactly
compareAnswers: function(trueAnswers, selectedAnswers, selectAny) {
if ( selectAny ) {
return $.inArray(selectedAnswers[0], trueAnswers) > -1;
} else {
// crafty array comparison (http://stackoverflow.com/a/7726509)
return ($(trueAnswers).not(selectedAnswers).length === 0 && $(selectedAnswers).not(trueAnswers).length === 0);
}
},
// Calculates knowledge level based on number of correct answers
calculateLevel: function(correctAnswers) {
var percent = (correctAnswers / questionCount).toFixed(2),
level = null;
if (plugin.method.inRange(0, 0.20, percent)) {
level = 4;
} else if (plugin.method.inRange(0.21, 0.40, percent)) {
level = 3;
} else if (plugin.method.inRange(0.41, 0.60, percent)) {
level = 2;
} else if (plugin.method.inRange(0.61, 0.80, percent)) {
level = 1;
} else if (plugin.method.inRange(0.81, 1.00, percent)) {
level = 0;
}
return level;
},
// Determines if percentage of correct values is within a level range
inRange: function(start, end, value) {
return (value >= start && value <= end);
}
};
plugin.init = function() {
// Setup quiz
plugin.method.setupQuiz.apply (null, [{callback: plugin.config.animationCallbacks.setupQuiz}]);
// Bind "start" button
$quizStarter.on('click', function(e) {
e.preventDefault();
if (!this.disabled && !$(this).hasClass('disabled')) {
plugin.method.startQuiz.apply (null, [{callback: plugin.config.animationCallbacks.startQuiz}]);
}
});
// Bind "try again" button
$(_element + ' ' + _tryAgainBtn).on('click', function(e) {
e.preventDefault();
plugin.method.resetQuiz(this, {callback: plugin.config.animationCallbacks.resetQuiz});
});
// Bind "check answer" buttons
$(_element + ' ' + _checkAnswerBtn).on('click', function(e) {
e.preventDefault();
plugin.method.checkAnswer(this, {callback: plugin.config.animationCallbacks.checkAnswer});
});
// Bind "back" buttons
$(_element + ' ' + _prevQuestionBtn).on('click', function(e) {
e.preventDefault();
plugin.method.backToQuestion(this, {callback: plugin.config.animationCallbacks.backToQuestion});
});
// Bind "next" buttons
$(_element + ' ' + _nextQuestionBtn).on('click', function(e) {
e.preventDefault();
plugin.method.nextQuestion(this, {callback: plugin.config.animationCallbacks.nextQuestion});
});
// Accessibility (WAI-ARIA).
var _qnid = $element.attr('id') + '-name';
$quizName.attr('id', _qnid);
$element.attr({
'aria-labelledby': _qnid,
'aria-live': 'polite',
'aria-relevant': 'additions',
'role': 'form'
});
$(_quizStarter + ', [href = "#"]').attr('role', 'button');
};
plugin.init();
};
$.fn.slickQuiz = function(options) {
return this.each(function() {
if (undefined === $(this).data('slickQuiz')) {
var plugin = new $.slickQuiz(this, options);
$(this).data('slickQuiz', plugin);
}
});
};
})(jQuery);