1
|
/*!
|
2
|
* SlickQuiz jQuery Plugin
|
3
|
* http://github.com/jewlofthelotus/SlickQuiz
|
4
|
*
|
5
|
* @updated October 25, 2014
|
6
|
* @version 1.5.20
|
7
|
*
|
8
|
* @author Julie Cameron - http://www.juliecameron.com
|
9
|
* @copyright (c) 2013 Quicken Loans - http://www.quickenloans.com
|
10
|
* @license MIT
|
11
|
*/
|
12
|
|
13
|
(function($){
|
14
|
$.slickQuiz = function(element, options) {
|
15
|
var plugin = this,
|
16
|
$element = $(element),
|
17
|
_element = '#' + $element.attr('id'),
|
18
|
|
19
|
defaults = {
|
20
|
checkAnswerText: 'Check My Answer!',
|
21
|
nextQuestionText: 'Next <i class="material-icons"></i>',
|
22
|
backButtonText: '',
|
23
|
completeQuizText: '',
|
24
|
tryAgainText: '',
|
25
|
questionCountText: 'Question %current of %total',
|
26
|
preventUnansweredText: 'You must select at least one answer.',
|
27
|
questionTemplateText: '%count. %text',
|
28
|
scoreTemplateText: '%score / %total',
|
29
|
nameTemplateText: '<span>Quiz: </span>%name',
|
30
|
skipStartButton: false,
|
31
|
numberOfQuestions: null,
|
32
|
randomSortQuestions: false,
|
33
|
randomSortAnswers: false,
|
34
|
preventUnanswered: false,
|
35
|
disableScore: false,
|
36
|
disableRanking: false,
|
37
|
scoreAsPercentage: false,
|
38
|
perQuestionResponseMessaging: true,
|
39
|
perQuestionResponseAnswers: false,
|
40
|
completionResponseMessaging: false,
|
41
|
displayQuestionCount: true, // Deprecate?
|
42
|
displayQuestionNumber: true, // Deprecate?
|
43
|
animationCallbacks: { // only for the methods that have jQuery animations offering callback
|
44
|
setupQuiz: function () {},
|
45
|
startQuiz: function () {},
|
46
|
resetQuiz: function () {},
|
47
|
checkAnswer: function () {},
|
48
|
nextQuestion: function () {},
|
49
|
backToQuestion: function () {},
|
50
|
completeQuiz: function () {}
|
51
|
},
|
52
|
events: {
|
53
|
onStartQuiz: function (options) {},
|
54
|
onCompleteQuiz: function (options) {} // reserved: options.questionCount, options.score
|
55
|
}
|
56
|
},
|
57
|
|
58
|
// Class Name Strings (Used for building quiz and for selectors)
|
59
|
questionCountClass = 'questionCount',
|
60
|
questionGroupClass = 'questions',
|
61
|
questionClass = 'question',
|
62
|
answersClass = 'answers',
|
63
|
responsesClass = 'responses',
|
64
|
completeClass = 'complete',
|
65
|
correctClass = 'correctResponse',
|
66
|
incorrectClass = 'incorrectResponse',
|
67
|
correctResponseClass = 'correct',
|
68
|
incorrectResponseClass = 'incorrect',
|
69
|
checkAnswerClass = 'checkAnswer',
|
70
|
nextQuestionClass = 'nextQuestion',
|
71
|
lastQuestionClass = 'lastQuestion',
|
72
|
backToQuestionClass = 'backToQuestion',
|
73
|
tryAgainClass = 'tryAgain',
|
74
|
|
75
|
// Sub-Quiz / Sub-Question Class Selectors
|
76
|
_questionCount = '.' + questionCountClass,
|
77
|
_questions = '.' + questionGroupClass,
|
78
|
_question = '.' + questionClass,
|
79
|
_answers = '.' + answersClass,
|
80
|
_answer = '.' + answersClass + ' li',
|
81
|
_responses = '.' + responsesClass,
|
82
|
_response = '.' + responsesClass + ' li',
|
83
|
_correct = '.' + correctClass,
|
84
|
_correctResponse = '.' + correctResponseClass,
|
85
|
_incorrectResponse = '.' + incorrectResponseClass,
|
86
|
_checkAnswerBtn = '.' + checkAnswerClass,
|
87
|
_nextQuestionBtn = '.' + nextQuestionClass,
|
88
|
_prevQuestionBtn = '.' + backToQuestionClass,
|
89
|
_tryAgainBtn = '.' + tryAgainClass,
|
90
|
|
91
|
// Top Level Quiz Element Class Selectors
|
92
|
_quizStarter = _element + ' .startQuiz',
|
93
|
_quizName = _element + ' .quizName',
|
94
|
_quizArea = _element + ' .quizArea',
|
95
|
_quizResults = _element + ' .quizResults',
|
96
|
_quizResultsCopy = _element + ' .quizResultsCopy',
|
97
|
_quizHeader = _element + ' .quizHeader',
|
98
|
_quizScore = _element + ' .quizScore',
|
99
|
_quizLevel = _element + ' .quizLevel',
|
100
|
|
101
|
// Top Level Quiz Element Objects
|
102
|
$quizStarter = $(_quizStarter),
|
103
|
$quizName = $(_quizName),
|
104
|
$quizArea = $(_quizArea),
|
105
|
$quizResults = $(_quizResults),
|
106
|
$quizResultsCopy = $(_quizResultsCopy),
|
107
|
$quizHeader = $(_quizHeader),
|
108
|
$quizScore = $(_quizScore),
|
109
|
$quizLevel = $(_quizLevel)
|
110
|
;
|
111
|
|
112
|
|
113
|
// Reassign user-submitted deprecated options
|
114
|
var depMsg = '';
|
115
|
|
116
|
if (options && typeof options.disableNext != 'undefined') {
|
117
|
if (typeof options.preventUnanswered == 'undefined') {
|
118
|
options.preventUnanswered = options.disableNext;
|
119
|
}
|
120
|
depMsg += 'The \'disableNext\' option has been deprecated, please use \'preventUnanswered\' in it\'s place.\n\n';
|
121
|
}
|
122
|
|
123
|
if (options && typeof options.disableResponseMessaging != 'undefined') {
|
124
|
if (typeof options.preventUnanswered == 'undefined') {
|
125
|
options.perQuestionResponseMessaging = options.disableResponseMessaging;
|
126
|
}
|
127
|
depMsg += 'The \'disableResponseMessaging\' option has been deprecated, please use' +
|
128
|
' \'perQuestionResponseMessaging\' and \'completionResponseMessaging\' in it\'s place.\n\n';
|
129
|
}
|
130
|
|
131
|
if (options && typeof options.randomSort != 'undefined') {
|
132
|
if (typeof options.randomSortQuestions == 'undefined') {
|
133
|
options.randomSortQuestions = options.randomSort;
|
134
|
}
|
135
|
if (typeof options.randomSortAnswers == 'undefined') {
|
136
|
options.randomSortAnswers = options.randomSort;
|
137
|
}
|
138
|
depMsg += 'The \'randomSort\' option has been deprecated, please use' +
|
139
|
' \'randomSortQuestions\' and \'randomSortAnswers\' in it\'s place.\n\n';
|
140
|
}
|
141
|
|
142
|
if (depMsg !== '') {
|
143
|
if (typeof console != 'undefined') {
|
144
|
console.warn(depMsg);
|
145
|
} else {
|
146
|
alert(depMsg);
|
147
|
}
|
148
|
}
|
149
|
// End of deprecation reassignment
|
150
|
|
151
|
|
152
|
plugin.config = $.extend(defaults, options);
|
153
|
|
154
|
// Set via json option or quizJSON variable (see slickQuiz-config.js)
|
155
|
var quizValues = (plugin.config.json ? plugin.config.json : typeof quizJSON != 'undefined' ? quizJSON : null);
|
156
|
|
157
|
// Get questions, possibly sorted randomly
|
158
|
var questions = plugin.config.randomSortQuestions ?
|
159
|
quizValues.questions.sort(function() { return (Math.round(Math.random())-0.5); }) :
|
160
|
quizValues.questions;
|
161
|
|
162
|
// Count the number of questions
|
163
|
var questionCount = questions.length;
|
164
|
|
165
|
// Select X number of questions to load if options is set
|
166
|
if (plugin.config.numberOfQuestions && questionCount >= plugin.config.numberOfQuestions) {
|
167
|
questions = questions.slice(0, plugin.config.numberOfQuestions);
|
168
|
questionCount = questions.length;
|
169
|
}
|
170
|
|
171
|
// some special private/internal methods
|
172
|
var internal = {method: {
|
173
|
// get a key whose notches are "resolved jQ deferred" objects; one per notch on the key
|
174
|
// think of the key as a house key with notches on it
|
175
|
getKey: function (notches) { // returns [], notches >= 1
|
176
|
var key = [];
|
177
|
for (i=0; i<notches; i++) key[i] = $.Deferred ();
|
178
|
return key;
|
179
|
},
|
180
|
|
181
|
// put the key in the door, if all the notches pass then you can turn the key and "go"
|
182
|
turnKeyAndGo: function (key, go) { // key = [], go = function ()
|
183
|
// when all the notches of the key are accepted (resolved) then the key turns and the engine (callback/go) starts
|
184
|
$.when.apply (null, key). then (function () {
|
185
|
go ();
|
186
|
});
|
187
|
},
|
188
|
|
189
|
// get one jQ
|
190
|
getKeyNotch: function (key, notch) { // notch >= 1, key = []
|
191
|
// key has several notches, numbered as 1, 2, 3, ... (no zero notch)
|
192
|
// we resolve and return the "jQ deferred" object at specified notch
|
193
|
return function () {
|
194
|
key[notch-1].resolve (); // it is ASSUMED that you initiated the key with enough notches
|
195
|
};
|
196
|
}
|
197
|
}};
|
198
|
|
199
|
plugin.method = {
|
200
|
// Sets up the questions and answers based on above array
|
201
|
setupQuiz: function(options) { // use 'options' object to pass args
|
202
|
var key, keyNotch, kN;
|
203
|
key = internal.method.getKey (3); // how many notches == how many jQ animations you will run
|
204
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
205
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
206
|
|
207
|
$quizName.hide().html(plugin.config.nameTemplateText
|
208
|
.replace('%name', quizValues.info.name) ).fadeIn(1000, kN(key,1));
|
209
|
$quizHeader.hide().prepend($('<div class="quizDescription">' + quizValues.info.main + '</div>')).fadeIn(1000, kN(key,2));
|
210
|
$quizResultsCopy.append(quizValues.info.results);
|
211
|
|
212
|
// add retry button to results view, if enabled
|
213
|
if (plugin.config.tryAgainText && plugin.config.tryAgainText !== '') {
|
214
|
$quizResultsCopy.append('<p><a class="button ' + tryAgainClass + '" href="#">' + plugin.config.tryAgainText + '</a></p>');
|
215
|
}
|
216
|
|
217
|
// Setup questions
|
218
|
var quiz = $('<ol class="' + questionGroupClass + '"></ol>'),
|
219
|
count = 1;
|
220
|
|
221
|
// Loop through questions object
|
222
|
for (i in questions) {
|
223
|
if (questions.hasOwnProperty(i)) {
|
224
|
var question = questions[i];
|
225
|
|
226
|
var questionHTML = $('<li class="' + questionClass +'" id="question' + (count - 1) + '"></li>');
|
227
|
|
228
|
if (plugin.config.displayQuestionCount) {
|
229
|
questionHTML.append('<div class="' + questionCountClass + '">' +
|
230
|
plugin.config.questionCountText
|
231
|
.replace('%current', '<span class="current">' + count + '</span>')
|
232
|
.replace('%total', '<span class="total">' +
|
233
|
questionCount + '</span>') + '</div>');
|
234
|
}
|
235
|
|
236
|
var formatQuestion = '';
|
237
|
if (plugin.config.displayQuestionNumber) {
|
238
|
formatQuestion = plugin.config.questionTemplateText
|
239
|
.replace('%count', count).replace('%text', question.q);
|
240
|
} else {
|
241
|
formatQuestion = question.q;
|
242
|
}
|
243
|
questionHTML.append('<h3>' + formatQuestion + '</h3>');
|
244
|
|
245
|
// Count the number of true values
|
246
|
var truths = 0;
|
247
|
for (i in question.a) {
|
248
|
if (question.a.hasOwnProperty(i)) {
|
249
|
answer = question.a[i];
|
250
|
if (answer.correct) {
|
251
|
truths++;
|
252
|
}
|
253
|
}
|
254
|
}
|
255
|
|
256
|
// Now let's append the answers with checkboxes or radios depending on truth count
|
257
|
var answerHTML = $('<ul class="' + answersClass + '"></ul>');
|
258
|
|
259
|
// Get the answers
|
260
|
var answers = plugin.config.randomSortAnswers ?
|
261
|
question.a.sort(function() { return (Math.round(Math.random())-0.5); }) :
|
262
|
question.a;
|
263
|
|
264
|
// prepare a name for the answer inputs based on the question
|
265
|
var selectAny = question.select_any ? question.select_any : false,
|
266
|
forceCheckbox = question.force_checkbox ? question.force_checkbox : false,
|
267
|
checkbox = (truths > 1 && !selectAny) || forceCheckbox,
|
268
|
inputName = $element.attr('id') + '_question' + (count - 1),
|
269
|
inputType = checkbox ? 'checkbox' : 'radio';
|
270
|
|
271
|
if( count == quizValues.questions.length ) {
|
272
|
nextQuestionClass = nextQuestionClass + ' ' + lastQuestionClass;
|
273
|
}
|
274
|
|
275
|
for (i in answers) {
|
276
|
if (answers.hasOwnProperty(i)) {
|
277
|
answer = answers[i],
|
278
|
optionId = inputName + '_' + i.toString();
|
279
|
|
280
|
// If question has >1 true answers and is not a select any, use checkboxes; otherwise, radios
|
281
|
var input = '<input id="' + optionId + '" name="' + inputName +
|
282
|
'" type="' + inputType + '" /> ';
|
283
|
|
284
|
var optionLabel = '<label for="' + optionId + '">' + answer.option + '</label>';
|
285
|
|
286
|
var answerContent = $('<li></li>')
|
287
|
.append(input)
|
288
|
.append(optionLabel);
|
289
|
answerHTML.append(answerContent);
|
290
|
}
|
291
|
}
|
292
|
|
293
|
// Append answers to question
|
294
|
questionHTML.append(answerHTML);
|
295
|
|
296
|
// If response messaging is NOT disabled, add it
|
297
|
if (plugin.config.perQuestionResponseMessaging || plugin.config.completionResponseMessaging) {
|
298
|
// Now let's append the correct / incorrect response messages
|
299
|
var responseHTML = $('<ul class="' + responsesClass + '"></ul>');
|
300
|
responseHTML.append('<li class="' + correctResponseClass + '">' + question.correct + '</li>');
|
301
|
responseHTML.append('<li class="' + incorrectResponseClass + '">' + question.incorrect + '</li>');
|
302
|
|
303
|
// Append responses to question
|
304
|
questionHTML.append(responseHTML);
|
305
|
}
|
306
|
|
307
|
// Appends check answer / back / next question buttons
|
308
|
if (plugin.config.backButtonText && plugin.config.backButtonText !== '') {
|
309
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + backToQuestionClass + '">' + plugin.config.backButtonText + '</a>');
|
310
|
}
|
311
|
|
312
|
var nextText = plugin.config.nextQuestionText;
|
313
|
if (plugin.config.completeQuizText && count == questionCount) {
|
314
|
nextText = plugin.config.completeQuizText;
|
315
|
}
|
316
|
|
317
|
// If we're not showing responses per question, show next question button and make it check the answer too
|
318
|
if (!plugin.config.perQuestionResponseMessaging) {
|
319
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + nextQuestionClass + ' ' + checkAnswerClass + '">' + nextText + '</a>');
|
320
|
} else {
|
321
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + nextQuestionClass + '">' + nextText + '</a>');
|
322
|
questionHTML.append('<a href="#" class="md-btn md-btn-large ' + checkAnswerClass + '">' + plugin.config.checkAnswerText + '</a>');
|
323
|
}
|
324
|
|
325
|
// Append question & answers to quiz
|
326
|
quiz.append(questionHTML);
|
327
|
|
328
|
count++;
|
329
|
}
|
330
|
}
|
331
|
|
332
|
// Add the quiz content to the page
|
333
|
$quizArea.append(quiz);
|
334
|
|
335
|
// Toggle the start button OR start the quiz if start button is disabled
|
336
|
if (plugin.config.skipStartButton || $quizStarter.length == 0) {
|
337
|
$quizStarter.hide();
|
338
|
plugin.method.startQuiz.apply (this, [{callback: plugin.config.animationCallbacks.startQuiz}]); // TODO: determine why 'this' is being passed as arg to startQuiz method
|
339
|
kN(key,3).apply (null, []);
|
340
|
} else {
|
341
|
$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
|
342
|
}
|
343
|
|
344
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
345
|
},
|
346
|
|
347
|
// Starts the quiz (hides start button and displays first question)
|
348
|
startQuiz: function(options) {
|
349
|
var key, keyNotch, kN;
|
350
|
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
|
351
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
352
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
353
|
|
354
|
function start(options) {
|
355
|
var firstQuestion = $(_element + ' ' + _questions + ' li').first();
|
356
|
if (firstQuestion.length) {
|
357
|
firstQuestion.fadeIn(500, function () {
|
358
|
if (options && options.callback) options.callback ();
|
359
|
});
|
360
|
}
|
361
|
}
|
362
|
|
363
|
if (plugin.config.skipStartButton || $quizStarter.length == 0) {
|
364
|
start({callback: kN(key,1)});
|
365
|
} else {
|
366
|
$quizStarter.fadeOut(300, function(){
|
367
|
start({callback: kN(key,1)}); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
|
368
|
});
|
369
|
}
|
370
|
|
371
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
372
|
|
373
|
if (plugin.config.events &&
|
374
|
plugin.config.events.onStartQuiz) {
|
375
|
plugin.config.events.onStartQuiz.apply (null, []);
|
376
|
}
|
377
|
},
|
378
|
|
379
|
// Resets (restarts) the quiz (hides results, resets inputs, and displays first question)
|
380
|
resetQuiz: function(startButton, options) {
|
381
|
var key, keyNotch, kN;
|
382
|
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
|
383
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
384
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
385
|
|
386
|
$quizResults.fadeOut(300, function() {
|
387
|
$(_element + ' input').prop('checked', false).prop('disabled', false);
|
388
|
|
389
|
$quizLevel.attr('class', 'quizLevel');
|
390
|
$(_element + ' ' + _question).removeClass(correctClass).removeClass(incorrectClass).remove(completeClass);
|
391
|
$(_element + ' ' + _answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass);
|
392
|
|
393
|
$(_element + ' ' + _question + ',' +
|
394
|
_element + ' ' + _responses + ',' +
|
395
|
_element + ' ' + _response + ',' +
|
396
|
_element + ' ' + _nextQuestionBtn + ',' +
|
397
|
_element + ' ' + _prevQuestionBtn
|
398
|
).hide();
|
399
|
|
400
|
$(_element + ' ' + _questionCount + ',' +
|
401
|
_element + ' ' + _answers + ',' +
|
402
|
_element + ' ' + _checkAnswerBtn
|
403
|
).show();
|
404
|
|
405
|
$quizArea.append($(_element + ' ' + _questions)).show();
|
406
|
|
407
|
kN(key,1).apply (null, []);
|
408
|
|
409
|
plugin.method.startQuiz({callback: plugin.config.animationCallbacks.startQuiz},$quizResults); // TODO: determine why $quizResults is being passed
|
410
|
});
|
411
|
|
412
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
413
|
},
|
414
|
|
415
|
// Validates the response selection(s), displays explanations & next question button
|
416
|
checkAnswer: function(checkButton, options) {
|
417
|
var key, keyNotch, kN;
|
418
|
key = internal.method.getKey (2); // how many notches == how many jQ animations you will run
|
419
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
420
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
421
|
|
422
|
var questionLI = $($(checkButton).parents(_question)[0]),
|
423
|
answerLIs = questionLI.find(_answers + ' li'),
|
424
|
answerSelects = answerLIs.find('input:checked'),
|
425
|
questionIndex = parseInt(questionLI.attr('id').replace(/(question)/, ''), 10),
|
426
|
answers = questions[questionIndex].a,
|
427
|
selectAny = questions[questionIndex].select_any ? questions[questionIndex].select_any : false;
|
428
|
|
429
|
answerLIs.addClass(incorrectResponseClass);
|
430
|
|
431
|
// Collect the true answers needed for a correct response
|
432
|
var trueAnswers = [];
|
433
|
for (i in answers) {
|
434
|
if (answers.hasOwnProperty(i)) {
|
435
|
var answer = answers[i],
|
436
|
index = parseInt(i, 10);
|
437
|
|
438
|
if (answer.correct) {
|
439
|
trueAnswers.push(index);
|
440
|
answerLIs.eq(index).removeClass(incorrectResponseClass).addClass(correctResponseClass);
|
441
|
}
|
442
|
}
|
443
|
}
|
444
|
|
445
|
// TODO: Now that we're marking answer LIs as correct / incorrect, we might be able
|
446
|
// to do all our answer checking at the same time
|
447
|
|
448
|
// NOTE: Collecting answer index for comparison aims to ensure that HTML entities
|
449
|
// and HTML elements that may be modified by the browser / other scrips match up
|
450
|
|
451
|
// Collect the answers submitted
|
452
|
var selectedAnswers = [];
|
453
|
answerSelects.each( function() {
|
454
|
var id = $(this).attr('id');
|
455
|
selectedAnswers.push(parseInt(id.replace(/(.*\_question\d{1,}_)/, ''), 10));
|
456
|
});
|
457
|
|
458
|
if (plugin.config.preventUnanswered && selectedAnswers.length === 0) {
|
459
|
alert(plugin.config.preventUnansweredText);
|
460
|
return false;
|
461
|
}
|
462
|
|
463
|
// Verify all/any true answers (and no false ones) were submitted
|
464
|
var correctResponse = plugin.method.compareAnswers(trueAnswers, selectedAnswers, selectAny);
|
465
|
|
466
|
if (correctResponse) {
|
467
|
questionLI.addClass(correctClass);
|
468
|
} else {
|
469
|
questionLI.addClass(incorrectClass);
|
470
|
}
|
471
|
|
472
|
// Toggle appropriate response (either for display now and / or on completion)
|
473
|
questionLI.find(correctResponse ? _correctResponse : _incorrectResponse).show();
|
474
|
|
475
|
// If perQuestionResponseMessaging is enabled, toggle response and navigation now
|
476
|
if (plugin.config.perQuestionResponseMessaging) {
|
477
|
$(checkButton).hide();
|
478
|
if (!plugin.config.perQuestionResponseAnswers) {
|
479
|
// Make sure answers don't highlight for a split second before they hide
|
480
|
questionLI.find(_answers).hide({
|
481
|
duration: 0,
|
482
|
complete: function() {
|
483
|
questionLI.addClass(completeClass);
|
484
|
}
|
485
|
});
|
486
|
} else {
|
487
|
questionLI.addClass(completeClass);
|
488
|
}
|
489
|
questionLI.find('input').prop('disabled', true);
|
490
|
questionLI.find(_responses).show();
|
491
|
questionLI.find(_nextQuestionBtn).fadeIn(300, kN(key,1)).css('display','inline-block');
|
492
|
questionLI.find(_prevQuestionBtn).fadeIn(300, kN(key,2)).css('display','inline-block');
|
493
|
if (!questionLI.find(_prevQuestionBtn).length) kN(key,2).apply (null, []); // 2nd notch on key must be passed even if there's no "back" button
|
494
|
} else {
|
495
|
kN(key,1).apply (null, []); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
|
496
|
kN(key,2).apply (null, []); // 2nd notch on key must be on both sides of if/else, otherwise key won't turn
|
497
|
}
|
498
|
|
499
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
500
|
},
|
501
|
|
502
|
// Moves to the next question OR completes the quiz if on last question
|
503
|
nextQuestion: function(nextButton, options) {
|
504
|
var key, keyNotch, kN;
|
505
|
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
|
506
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
507
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
508
|
|
509
|
var currentQuestion = $($(nextButton).parents(_question)[0]),
|
510
|
nextQuestion = currentQuestion.next(_question),
|
511
|
answerInputs = currentQuestion.find('input:checked');
|
512
|
|
513
|
// If response messaging has been disabled or moved to completion,
|
514
|
// make sure we have an answer if we require it, let checkAnswer handle the alert messaging
|
515
|
if (plugin.config.preventUnanswered && answerInputs.length === 0) {
|
516
|
return false;
|
517
|
}
|
518
|
|
519
|
if (nextQuestion.length) {
|
520
|
currentQuestion.fadeOut(300, function(){
|
521
|
nextQuestion.find(_prevQuestionBtn).show().end().fadeIn(500, kN(key,1));
|
522
|
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
|
523
|
});
|
524
|
} else {
|
525
|
kN(key,1).apply (null, []); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
|
526
|
plugin.method.completeQuiz({callback: plugin.config.animationCallbacks.completeQuiz});
|
527
|
}
|
528
|
|
529
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
530
|
},
|
531
|
|
532
|
// Go back to the last question
|
533
|
backToQuestion: function(backButton, options) {
|
534
|
var key, keyNotch, kN;
|
535
|
key = internal.method.getKey (2); // how many notches == how many jQ animations you will run
|
536
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
537
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
538
|
|
539
|
var questionLI = $($(backButton).parents(_question)[0]),
|
540
|
responses = questionLI.find(_responses);
|
541
|
|
542
|
// Back to question from responses
|
543
|
if (responses.css('display') === 'block' ) {
|
544
|
questionLI.find(_responses).fadeOut(300, function(){
|
545
|
questionLI.removeClass(correctClass).removeClass(incorrectClass).removeClass(completeClass);
|
546
|
questionLI.find(_responses + ', ' + _response).hide();
|
547
|
questionLI.find(_answers).show();
|
548
|
questionLI.find(_answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass);
|
549
|
questionLI.find('input').prop('disabled', false);
|
550
|
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
|
551
|
questionLI.find(_checkAnswerBtn).fadeIn(500, kN(key,2));
|
552
|
questionLI.find(_nextQuestionBtn).hide();
|
553
|
|
554
|
// if question is first, don't show back button on question
|
555
|
if (questionLI.attr('id') != 'question0') {
|
556
|
questionLI.find(_prevQuestionBtn).show();
|
557
|
} else {
|
558
|
questionLI.find(_prevQuestionBtn).hide();
|
559
|
}
|
560
|
});
|
561
|
|
562
|
// Back to previous question
|
563
|
} else {
|
564
|
var prevQuestion = questionLI.prev(_question);
|
565
|
|
566
|
questionLI.fadeOut(300, function() {
|
567
|
prevQuestion.removeClass(correctClass).removeClass(incorrectClass).removeClass(completeClass);
|
568
|
prevQuestion.find(_responses + ', ' + _response).hide();
|
569
|
prevQuestion.find(_answers).show();
|
570
|
prevQuestion.find(_answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass);
|
571
|
prevQuestion.find('input').prop('disabled', false);
|
572
|
prevQuestion.find(_nextQuestionBtn).hide();
|
573
|
prevQuestion.find(_checkAnswerBtn).show();
|
574
|
|
575
|
if (prevQuestion.attr('id') != 'question0') {
|
576
|
prevQuestion.find(_prevQuestionBtn).show();
|
577
|
} else {
|
578
|
prevQuestion.find(_prevQuestionBtn).hide();
|
579
|
}
|
580
|
|
581
|
prevQuestion.fadeIn(500, kN(key,1));
|
582
|
kN(key,2).apply (null, []); // 2nd notch on key must be on both sides of if/else, otherwise key won't turn
|
583
|
});
|
584
|
}
|
585
|
|
586
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
587
|
},
|
588
|
|
589
|
// Hides all questions, displays the final score and some conclusive information
|
590
|
completeQuiz: function(options) {
|
591
|
var key, keyNotch, kN;
|
592
|
key = internal.method.getKey (1); // how many notches == how many jQ animations you will run
|
593
|
keyNotch = internal.method.getKeyNotch; // a function that returns a jQ animation callback function
|
594
|
kN = keyNotch; // you specify the notch, you get a callback function for your animation
|
595
|
|
596
|
var score = $(_element + ' ' + _correct).length,
|
597
|
displayScore = score;
|
598
|
if (plugin.config.scoreAsPercentage) {
|
599
|
displayScore = (score / questionCount).toFixed(2)*100 + "%";
|
600
|
}
|
601
|
|
602
|
if (plugin.config.disableScore) {
|
603
|
$(_quizScore).remove()
|
604
|
} else {
|
605
|
$(_quizScore + ' span').html(plugin.config.scoreTemplateText
|
606
|
.replace('%score', displayScore).replace('%total', questionCount));
|
607
|
}
|
608
|
|
609
|
if (plugin.config.disableRanking) {
|
610
|
$(_quizLevel).remove()
|
611
|
} else {
|
612
|
var levels = [
|
613
|
quizValues.info.level1, // 80-100%
|
614
|
quizValues.info.level2, // 60-79%
|
615
|
quizValues.info.level3, // 40-59%
|
616
|
quizValues.info.level4, // 20-39%
|
617
|
quizValues.info.level5 // 0-19%
|
618
|
],
|
619
|
levelRank = plugin.method.calculateLevel(score),
|
620
|
levelText = $.isNumeric(levelRank) ? levels[levelRank] : '';
|
621
|
|
622
|
$(_quizLevel + ' span').html(levelText);
|
623
|
$(_quizLevel).addClass('level' + levelRank);
|
624
|
}
|
625
|
|
626
|
$quizArea.fadeOut(300, function() {
|
627
|
// If response messaging is set to show upon quiz completion, show it now
|
628
|
if (plugin.config.completionResponseMessaging) {
|
629
|
$(_element + ' .button:not(' + _tryAgainBtn + '), ' + _element + ' ' + _questionCount).hide();
|
630
|
$(_element + ' ' + _question + ', ' + _element + ' ' + _answers + ', ' + _element + ' ' + _responses).show();
|
631
|
$quizResults.append($(_element + ' ' + _questions)).fadeIn(500, kN(key,1));
|
632
|
} else {
|
633
|
$quizResults.fadeIn(500, kN(key,1)); // 1st notch on key must be on both sides of if/else, otherwise key won't turn
|
634
|
}
|
635
|
});
|
636
|
|
637
|
internal.method.turnKeyAndGo (key, options && options.callback ? options.callback : function () {});
|
638
|
|
639
|
if (plugin.config.events &&
|
640
|
plugin.config.events.onCompleteQuiz) {
|
641
|
plugin.config.events.onCompleteQuiz.apply (null, [{
|
642
|
questionCount: questionCount,
|
643
|
score: score
|
644
|
}]);
|
645
|
}
|
646
|
},
|
647
|
|
648
|
// Compares selected responses with true answers, returns true if they match exactly
|
649
|
compareAnswers: function(trueAnswers, selectedAnswers, selectAny) {
|
650
|
if ( selectAny ) {
|
651
|
return $.inArray(selectedAnswers[0], trueAnswers) > -1;
|
652
|
} else {
|
653
|
// crafty array comparison (http://stackoverflow.com/a/7726509)
|
654
|
return ($(trueAnswers).not(selectedAnswers).length === 0 && $(selectedAnswers).not(trueAnswers).length === 0);
|
655
|
}
|
656
|
},
|
657
|
|
658
|
// Calculates knowledge level based on number of correct answers
|
659
|
calculateLevel: function(correctAnswers) {
|
660
|
var percent = (correctAnswers / questionCount).toFixed(2),
|
661
|
level = null;
|
662
|
|
663
|
if (plugin.method.inRange(0, 0.20, percent)) {
|
664
|
level = 4;
|
665
|
} else if (plugin.method.inRange(0.21, 0.40, percent)) {
|
666
|
level = 3;
|
667
|
} else if (plugin.method.inRange(0.41, 0.60, percent)) {
|
668
|
level = 2;
|
669
|
} else if (plugin.method.inRange(0.61, 0.80, percent)) {
|
670
|
level = 1;
|
671
|
} else if (plugin.method.inRange(0.81, 1.00, percent)) {
|
672
|
level = 0;
|
673
|
}
|
674
|
|
675
|
return level;
|
676
|
},
|
677
|
|
678
|
// Determines if percentage of correct values is within a level range
|
679
|
inRange: function(start, end, value) {
|
680
|
return (value >= start && value <= end);
|
681
|
}
|
682
|
};
|
683
|
|
684
|
plugin.init = function() {
|
685
|
// Setup quiz
|
686
|
plugin.method.setupQuiz.apply (null, [{callback: plugin.config.animationCallbacks.setupQuiz}]);
|
687
|
|
688
|
// Bind "start" button
|
689
|
$quizStarter.on('click', function(e) {
|
690
|
e.preventDefault();
|
691
|
|
692
|
if (!this.disabled && !$(this).hasClass('disabled')) {
|
693
|
plugin.method.startQuiz.apply (null, [{callback: plugin.config.animationCallbacks.startQuiz}]);
|
694
|
}
|
695
|
});
|
696
|
|
697
|
// Bind "try again" button
|
698
|
$(_element + ' ' + _tryAgainBtn).on('click', function(e) {
|
699
|
e.preventDefault();
|
700
|
plugin.method.resetQuiz(this, {callback: plugin.config.animationCallbacks.resetQuiz});
|
701
|
});
|
702
|
|
703
|
// Bind "check answer" buttons
|
704
|
$(_element + ' ' + _checkAnswerBtn).on('click', function(e) {
|
705
|
e.preventDefault();
|
706
|
plugin.method.checkAnswer(this, {callback: plugin.config.animationCallbacks.checkAnswer});
|
707
|
});
|
708
|
|
709
|
// Bind "back" buttons
|
710
|
$(_element + ' ' + _prevQuestionBtn).on('click', function(e) {
|
711
|
e.preventDefault();
|
712
|
plugin.method.backToQuestion(this, {callback: plugin.config.animationCallbacks.backToQuestion});
|
713
|
});
|
714
|
|
715
|
// Bind "next" buttons
|
716
|
$(_element + ' ' + _nextQuestionBtn).on('click', function(e) {
|
717
|
e.preventDefault();
|
718
|
plugin.method.nextQuestion(this, {callback: plugin.config.animationCallbacks.nextQuestion});
|
719
|
});
|
720
|
|
721
|
// Accessibility (WAI-ARIA).
|
722
|
var _qnid = $element.attr('id') + '-name';
|
723
|
$quizName.attr('id', _qnid);
|
724
|
$element.attr({
|
725
|
'aria-labelledby': _qnid,
|
726
|
'aria-live': 'polite',
|
727
|
'aria-relevant': 'additions',
|
728
|
'role': 'form'
|
729
|
});
|
730
|
$(_quizStarter + ', [href = "#"]').attr('role', 'button');
|
731
|
};
|
732
|
|
733
|
plugin.init();
|
734
|
};
|
735
|
|
736
|
$.fn.slickQuiz = function(options) {
|
737
|
return this.each(function() {
|
738
|
if (undefined === $(this).data('slickQuiz')) {
|
739
|
var plugin = new $.slickQuiz(this, options);
|
740
|
$(this).data('slickQuiz', plugin);
|
741
|
}
|
742
|
});
|
743
|
};
|
744
|
})(jQuery);
|