744 lines
38 KiB
JavaScript
Executable File
744 lines
38 KiB
JavaScript
Executable File
/*!
|
|
* SlickQuiz jQuery Plugin
|
|
* http://github.com/jewlofthelotus/SlickQuiz
|
|
*
|
|
* @updated October 25, 2014
|
|
* @version 1.5.20
|
|
*
|
|
* @author Julie Cameron - http://www.juliecameron.com
|
|
* @copyright (c) 2013 Quicken Loans - http://www.quickenloans.com
|
|
* @license MIT
|
|
*/
|
|
|
|
(function($){
|
|
$.slickQuiz = function(element, options) {
|
|
var plugin = this,
|
|
$element = $(element),
|
|
_element = '#' + $element.attr('id'),
|
|
|
|
defaults = {
|
|
checkAnswerText: 'Check My Answer!',
|
|
nextQuestionText: 'Next <i class="material-icons"></i>',
|
|
backButtonText: '',
|
|
completeQuizText: '',
|
|
tryAgainText: '',
|
|
questionCountText: 'Question %current of %total',
|
|
preventUnansweredText: 'You must select at least one answer.',
|
|
questionTemplateText: '%count. %text',
|
|
scoreTemplateText: '%score / %total',
|
|
nameTemplateText: '<span>Quiz: </span>%name',
|
|
skipStartButton: false,
|
|
numberOfQuestions: null,
|
|
randomSortQuestions: false,
|
|
randomSortAnswers: false,
|
|
preventUnanswered: false,
|
|
disableScore: false,
|
|
disableRanking: false,
|
|
scoreAsPercentage: false,
|
|
perQuestionResponseMessaging: true,
|
|
perQuestionResponseAnswers: false,
|
|
completionResponseMessaging: false,
|
|
displayQuestionCount: true, // Deprecate?
|
|
displayQuestionNumber: true, // Deprecate?
|
|
animationCallbacks: { // only for the methods that have jQuery animations offering callback
|
|
setupQuiz: function () {},
|
|
startQuiz: function () {},
|
|
resetQuiz: function () {},
|
|
checkAnswer: function () {},
|
|
nextQuestion: function () {},
|
|
backToQuestion: function () {},
|
|
completeQuiz: function () {}
|
|
},
|
|
events: {
|
|
onStartQuiz: function (options) {},
|
|
onCompleteQuiz: function (options) {} // reserved: options.questionCount, options.score
|
|
}
|
|
},
|
|
|
|
// Class Name Strings (Used for building quiz and for selectors)
|
|
questionCountClass = 'questionCount',
|
|
questionGroupClass = 'questions',
|
|
questionClass = 'question',
|
|
answersClass = 'answers',
|
|
responsesClass = 'responses',
|
|
completeClass = 'complete',
|
|
correctClass = 'correctResponse',
|
|
incorrectClass = 'incorrectResponse',
|
|
correctResponseClass = 'correct',
|
|
incorrectResponseClass = 'incorrect',
|
|
checkAnswerClass = 'checkAnswer',
|
|
nextQuestionClass = 'nextQuestion',
|
|
lastQuestionClass = 'lastQuestion',
|
|
backToQuestionClass = 'backToQuestion',
|
|
tryAgainClass = 'tryAgain',
|
|
|
|
// Sub-Quiz / Sub-Question Class Selectors
|
|
_questionCount = '.' + questionCountClass,
|
|
_questions = '.' + questionGroupClass,
|
|
_question = '.' + questionClass,
|
|
_answers = '.' + answersClass,
|
|
_answer = '.' + answersClass + ' li',
|
|
_responses = '.' + responsesClass,
|
|
_response = '.' + responsesClass + ' li',
|
|
_correct = '.' + correctClass,
|
|
_correctResponse = '.' + correctResponseClass,
|
|
_incorrectResponse = '.' + incorrectResponseClass,
|
|
_checkAnswerBtn = '.' + checkAnswerClass,
|
|
_nextQuestionBtn = '.' + nextQuestionClass,
|
|
_prevQuestionBtn = '.' + backToQuestionClass,
|
|
_tryAgainBtn = '.' + tryAgainClass,
|
|
|
|
// Top Level Quiz Element Class Selectors
|
|
_quizStarter = _element + ' .startQuiz',
|
|
_quizName = _element + ' .quizName',
|
|
_quizArea = _element + ' .quizArea',
|
|
_quizResults = _element + ' .quizResults',
|
|
_quizResultsCopy = _element + ' .quizResultsCopy',
|
|
_quizHeader = _element + ' .quizHeader',
|
|
_quizScore = _element + ' .quizScore',
|
|
_quizLevel = _element + ' .quizLevel',
|
|
|
|
// Top Level Quiz Element Objects
|
|
$quizStarter = $(_quizStarter),
|
|
$quizName = $(_quizName),
|
|
$quizArea = $(_quizArea),
|
|
$quizResults = $(_quizResults),
|
|
$quizResultsCopy = $(_quizResultsCopy),
|
|
$quizHeader = $(_quizHeader),
|
|
$quizScore = $(_quizScore),
|
|
$quizLevel = $(_quizLevel)
|
|
;
|
|
|
|
|
|
// Reassign user-submitted deprecated options
|
|
var depMsg = '';
|
|
|
|
if (options && typeof options.disableNext != 'undefined') {
|
|
if (typeof options.preventUnanswered == 'undefined') {
|
|
options.preventUnanswered = options.disableNext;
|
|
}
|
|
depMsg += 'The \'disableNext\' option has been deprecated, please use \'preventUnanswered\' in it\'s place.\n\n';
|
|
}
|
|
|
|
if (options && typeof options.disableResponseMessaging != 'undefined') {
|
|
if (typeof options.preventUnanswered == 'undefined') {
|
|
options.perQuestionResponseMessaging = options.disableResponseMessaging;
|
|
}
|
|
depMsg += 'The \'disableResponseMessaging\' option has been deprecated, please use' +
|
|
' \'perQuestionResponseMessaging\' and \'completionResponseMessaging\' in it\'s place.\n\n';
|
|
}
|
|
|
|
if (options && typeof options.randomSort != 'undefined') {
|
|
if (typeof options.randomSortQuestions == 'undefined') {
|
|
options.randomSortQuestions = options.randomSort;
|
|
}
|
|
if (typeof options.randomSortAnswers == 'undefined') {
|
|
options.randomSortAnswers = options.randomSort;
|
|
}
|
|
depMsg += 'The \'randomSort\' option has been deprecated, please use' +
|
|
' \'randomSortQuestions\' and \'randomSortAnswers\' in it\'s place.\n\n';
|
|
}
|
|
|
|
if (depMsg !== '') {
|
|
if (typeof console != 'undefined') {
|
|
console.warn(depMsg);
|
|
} else {
|
|
alert(depMsg);
|
|
}
|
|
}
|
|
// End of deprecation reassignment
|
|
|
|
|
|
plugin.config = $.extend(defaults, options);
|
|
|
|
// Set via json option or quizJSON variable (see slickQuiz-config.js)
|
|
var quizValues = (plugin.config.json ? plugin.config.json : typeof quizJSON != 'undefined' ? quizJSON : null);
|
|
|
|
// Get questions, possibly sorted randomly
|
|
var questions = plugin.config.randomSortQuestions ?
|
|
quizValues.questions.sort(function() { return (Math.round(Math.random())-0.5); }) :
|
|
quizValues.questions;
|
|
|
|
// Count the number of questions
|
|
var questionCount = questions.length;
|
|
|
|
// Select X number of questions to load if options is set
|
|
if (plugin.config.numberOfQuestions && questionCount >= plugin.config.numberOfQuestions) {
|
|
questions = questions.slice(0, plugin.config.numberOfQuestions);
|
|
questionCount = questions.length;
|
|
}
|
|
|
|
// some special private/internal methods
|
|
var internal = {method: {
|
|
// get a key whose notches are "resolved jQ deferred" objects; one per notch on the key
|
|
// think of the key as a house key with notches on it
|
|
getKey: function (notches) { // returns [], notches >= 1
|
|
var key = [];
|
|
for (i=0; i<notches; i++) key[i] = $.Deferred ();
|
|
return key;
|
|
},
|
|
|
|
// put the key in the door, if all the notches pass then you can turn the key and "go"
|
|
turnKeyAndGo: function (key, go) { // key = [], go = function ()
|
|
// when all the notches of the key are accepted (resolved) then the key turns and the engine (callback/go) starts
|
|
$.when.apply (null, key). then (function () {
|
|
go ();
|
|
});
|
|
},
|
|
|
|
// get one jQ
|
|
getKeyNotch: function (key, notch) { // notch >= 1, key = []
|
|
// key has several notches, numbered as 1, 2, 3, ... (no zero notch)
|
|
// we resolve and return the "jQ deferred" object at specified notch
|
|
return function () {
|
|
key[notch-1].resolve (); // it is ASSUMED that you initiated the key with enough notches
|
|
};
|
|
}
|
|
}};
|
|
|
|
plugin.method = {
|
|
// Sets up the questions and answers based on above array
|
|
setupQuiz: function(options) { // use 'options' object to pass args
|
|
var key, keyNotch, kN;
|
|
key = internal.method.getKey (3); // 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
|
|
|
|
$quizName.hide().html(plugin.config.nameTemplateText
|
|
.replace('%name', quizValues.info.name) ).fadeIn(1000, kN(key,1));
|
|
$quizHeader.hide().prepend($('<div class="quizDescription">' + quizValues.info.main + '</div>')).fadeIn(1000, kN(key,2));
|
|
$quizResultsCopy.append(quizValues.info.results);
|
|
|
|
// add retry button to results view, if enabled
|
|
if (plugin.config.tryAgainText && plugin.config.tryAgainText !== '') {
|
|
$quizResultsCopy.append('<p><a class="button ' + tryAgainClass + '" href="#">' + plugin.config.tryAgainText + '</a></p>');
|
|
}
|
|
|
|
// Setup questions
|
|
var quiz = $('<ol class="' + questionGroupClass + '"></ol>'),
|
|
count = 1;
|
|
|
|
// Loop through questions object
|
|
for (i in questions) {
|
|
if (questions.hasOwnProperty(i)) {
|
|
var question = questions[i];
|
|
|
|
var questionHTML = $('<li class="' + questionClass +'" id="question' + (count - 1) + '"></li>');
|
|
|
|
if (plugin.config.displayQuestionCount) {
|
|
questionHTML.append('<div class="' + questionCountClass + '">' +
|
|
plugin.config.questionCountText
|
|
.replace('%current', '<span class="current">' + count + '</span>')
|
|
.replace('%total', '<span class="total">' +
|
|
questionCount + '</span>') + '</div>');
|
|
}
|
|
|
|
var formatQuestion = '';
|
|
if (plugin.config.displayQuestionNumber) {
|
|
formatQuestion = plugin.config.questionTemplateText
|
|
.replace('%count', count).replace('%text', question.q);
|
|
} else {
|
|
formatQuestion = question.q;
|
|
}
|
|
questionHTML.append('<h3>' + formatQuestion + '</h3>');
|
|
|
|
// 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 = $('<ul class="' + answersClass + '"></ul>');
|
|
|
|
// 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 = '<input id="' + optionId + '" name="' + inputName +
|
|
'" type="' + inputType + '" /> ';
|
|
|
|
var optionLabel = '<label for="' + optionId + '">' + answer.option + '</label>';
|
|
|
|
var answerContent = $('<li></li>')
|
|
.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 = $('<ul class="' + responsesClass + '"></ul>');
|
|
responseHTML.append('<li class="' + correctResponseClass + '">' + question.correct + '</li>');
|
|
responseHTML.append('<li class="' + incorrectResponseClass + '">' + question.incorrect + '</li>');
|
|
|
|
// Append responses to question
|
|
questionHTML.append(responseHTML);
|
|
}
|
|
|
|
// Appends check answer / back / next question buttons
|
|
if (plugin.config.backButtonText && plugin.config.backButtonText !== '') {
|
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + backToQuestionClass + '">' + plugin.config.backButtonText + '</a>');
|
|
}
|
|
|
|
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('<a href="#" class="md-btn md-btn-large ' + nextQuestionClass + ' ' + checkAnswerClass + '">' + nextText + '</a>');
|
|
} else {
|
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + nextQuestionClass + '">' + nextText + '</a>');
|
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + checkAnswerClass + '">' + plugin.config.checkAnswerText + '</a>');
|
|
}
|
|
|
|
// 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)).css('display', 'inline-block'); // 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)).css('display','inline-block');
|
|
questionLI.find(_prevQuestionBtn).fadeIn(300, kN(key,2)).css('display','inline-block');
|
|
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); |