Project

General

Profile

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">&#xE5CC;</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);
(1-1/2)