From cda2ed8c3a5528cd9a9cde369af7d88f6ba5a0de Mon Sep 17 00:00:00 2001 From: jewlofthelotus Date: Sat, 9 Aug 2014 06:09:44 +0000 Subject: [PATCH] closes #73, closes #70, closes #50 git-svn-id: http://plugins.svn.wordpress.org/slickquiz/trunk@962877 b8457f37-d9ea-0310-8a92-e5e31aec5664 --- css/admin.css | 3 +- css/front.css | 2 + js/admin.js | 54 +++++++++--- php/slickquiz-edit.php | 6 +- php/slickquiz-front.php | 9 +- php/slickquiz-helper.php | 53 +++++------ php/slickquiz-new.php | 8 ++ php/slickquiz-options.php | 47 ++++++++-- php/slickquiz-preview.php | 5 +- readme.txt | 16 +++- slickquiz.php | 2 +- slickquiz/README.md | 49 +++++++++-- slickquiz/js/slickQuiz.js | 179 +++++++++++++++++++++++++------------- 13 files changed, 314 insertions(+), 119 deletions(-) diff --git a/css/admin.css b/css/admin.css index 885b35d..08f64b6 100644 --- a/css/admin.css +++ b/css/admin.css @@ -75,7 +75,8 @@ .slickQuiz .quizFormWrapper .question label { display: block; } -.slickQuiz .quizFormWrapper .selectAny label { +.slickQuiz .quizFormWrapper .selectAny label, +.slickQuiz .quizFormWrapper .forceCheckbox label { display: inline-block; } diff --git a/css/front.css b/css/front.css index eb59bd3..4fa8e15 100644 --- a/css/front.css +++ b/css/front.css @@ -96,6 +96,8 @@ display: block; font-weight: bold; } + +.slickQuizWrapper ul.answers li.correct, .slickQuizWrapper ul.responses li.correct p span { color: #6C9F2E; } diff --git a/js/admin.js b/js/admin.js index d421b9b..7cddb2a 100644 --- a/js/admin.js +++ b/js/admin.js @@ -1,4 +1,3 @@ -// Mortgage Knowledge Quiz jQuery(document).ready(function($) { var adminPath = location.pathname.replace(/wp-admin.*/, 'wp-admin/'); @@ -23,6 +22,9 @@ jQuery(document).ready(function($) { // If editing a quiz, quizJSON will exist var quizValues = (typeof quizJSON != 'undefined' ? quizJSON : null); + // See if we need to hide and unrequire the ranking levels + var rankingDisabled = (typeof disableRanking != 'undefined' ? disableRanking : null); + var defaults = { quizArea: 'div.quizFormWrapper', quizForm: 'form.quizForm', @@ -127,13 +129,18 @@ jQuery(document).ready(function($) { addDefaultFields: function(fieldset) { for (f in defaults.fields) { // get field info - if quizJSON exists, use quizJSON data - field = defaults.fields[f]; + field = defaults.fields[f]; inputName = field.q.replace(/\W/g,''); - required = field.required ? defaults.requiredString : ''; nameAndId = 'name="' + inputName + '" id="' + inputName + '"'; placeholder = field.placeholder ? 'placeholder="' + field.placeholder + '"' : ''; + required = field.required ? defaults.requiredString : ''; - if (quizValues != null) { + if (rankingDisabled && /^level/.test(field.jsonName)) { + required = ''; + field.required = false; + } + + if (quizValues != null && quizValues.info[field.jsonName]) { value = plugin.formHelper.htmlspecialchars(quizValues.info[field.jsonName]); } else { value = ''; @@ -142,7 +149,7 @@ jQuery(document).ready(function($) { // Setup Field Container defaultQuestionHTML = $('
'); - // Add Input Group Label (e.g. "Knowledge Levels") + // Add Input Group Label (e.g. "Ranking Levels") if (field.label) { defaultQuestionHTML.append(''); } @@ -152,6 +159,10 @@ jQuery(document).ready(function($) { defaultQuestionHTML.append('' + field.descGroup + ''); } + if (rankingDisabled && /^level1/.test(field.jsonName)) { + defaultQuestionHTML.append('

Ranking levels are currently disabled and will not appear in the quiz.

'); + } + // Add Input Label defaultQuestionHTML.append(' '); @@ -220,11 +231,14 @@ jQuery(document).ready(function($) { plugin.formBuilder.addAnswer(newAnswerHTML, fieldGroup.a[f]); } + plugin.formBuilder.addForceCheckbox(newAnswerHTML, fieldGroup); plugin.formBuilder.addSelectAny(newAnswerHTML, fieldGroup); + } else { // Add blank answers to NEW quiz form question plugin.formBuilder.addAnswer(newAnswerHTML.children('a')[0]); plugin.formBuilder.addAnswer(newAnswerHTML.children('a')[0]); + plugin.formBuilder.addForceCheckbox(newAnswerHTML.children('a')[0]); plugin.formBuilder.addSelectAny(newAnswerHTML.children('a')[0]); } @@ -252,15 +266,29 @@ jQuery(document).ready(function($) { addAnswerLink = fieldGroup ? $(element) : $(element).parent(); var anyAnswerHTML = '
' - + ' ' - + '
' - + 'If you have more than one correct answer for this quesiton, by default the user must choose all correct answers to pass.
' + + '
' + + 'If you have more than one correct answer for this question, by default the user must choose all correct answers to pass.
' + 'Checking this box will change the question so that choosing any single correct answer will result in a correct response. ' + '
'; addAnswerLink.after($(anyAnswerHTML).hide().fadeIn(800)); }, + // Adds "force checkbox" answer option to the selected question + addForceCheckbox: function(element, fieldGroup) { + addAnswerLink = fieldGroup ? $(element) : $(element).parent(); + + var forceCheckboxHTML = '
' + + '
' + + 'If you only have one correct answer for this quesiton, by default the quiz will display radio buttons (which only allow a single selection).
' + + 'Checking this box will force the question to display checkboxes, which obscures the fact that there is a single answer from the user. ' + + '
'; + + addAnswerLink.after($(forceCheckboxHTML).hide().fadeIn(800)); + }, + // Return toggle elements addToggles: function(position) { toggles = '' @@ -630,6 +658,7 @@ jQuery(document).ready(function($) { incorrectResponse = $($(questionSet).children('.incorrect').children('textarea')[0]); answers = $($(questionSet).children('.answer')); selectAny = $($(questionSet).find('.selectAny input[type="checkbox"]')[0]).attr('checked'); + forceCheckbox = $($(questionSet).find('.forceCheckbox input[type="checkbox"]')[0]).attr('checked'); question = {"a": []}; correctAnswers = false; @@ -663,10 +692,11 @@ jQuery(document).ready(function($) { if ($.inArray(false, questionValidations) > -1) { valid = false; } else { - question["q"] = questionInput.attr('value'); - question["correct"] = correctResponse.attr('value'); - question["incorrect"] = incorrectResponse.attr('value'); - question["select_any"] = selectAny ? true : false; + question["q"] = questionInput.attr('value'); + question["correct"] = correctResponse.attr('value'); + question["incorrect"] = incorrectResponse.attr('value'); + question["select_any"] = selectAny ? true : false; + question["force_checkbox"] = forceCheckbox ? true : false; quizJson["questions"].push(question); } }); diff --git a/php/slickquiz-edit.php b/php/slickquiz-edit.php index 1a46cb2..676cf4b 100644 --- a/php/slickquiz-edit.php +++ b/php/slickquiz-edit.php @@ -16,7 +16,10 @@ class SlickQuizEdit extends SlickQuizModel // Constructor function __construct() { - global $quiz; + global $quiz, $pluginOptions; + + $this->get_admin_options(); + $quiz = $this->get_quiz_by_id( $_GET['id'] ); // Add Form JS @@ -109,4 +112,5 @@ function show_alert_messages() diff --git a/php/slickquiz-front.php b/php/slickquiz-front.php index 291895d..8330528 100644 --- a/php/slickquiz-front.php +++ b/php/slickquiz-front.php @@ -87,9 +87,12 @@ function load_quiz_script() randomSortAnswers: ' . ( $this->get_admin_option( 'random_sort_answers' ) == '1' ? 'true' : 'false' ) . ', preventUnanswered: ' . ( $this->get_admin_option( 'disable_next' ) == '1' ? 'true' : 'false' ) . ', perQuestionResponseMessaging: ' . ( $this->get_admin_option( 'perquestion_responses' ) == '1' ? 'true' : 'false' ) . ', + perQuestionResponseAnswers: ' . ( $this->get_admin_option( 'perquestion_response_answers' ) == '1' ? 'true' : 'false' ) . ', completionResponseMessaging: ' . ( $this->get_admin_option( 'completion_responses' ) == '1' ? 'true' : 'false' ) . ', displayQuestionCount: ' . ( $this->get_admin_option( 'question_count' ) == '1' ? 'true' : 'false' ) . ', - displayQuestionNumber: ' . ( $this->get_admin_option( 'question_number' ) == '1' ? 'true' : 'false' ) . ' + displayQuestionNumber: ' . ( $this->get_admin_option( 'question_number' ) == '1' ? 'true' : 'false' ) . ', + disableScore: ' . ( $this->get_admin_option( 'disable_score' ) == '1' ? 'true' : 'false' ) . ', + disableRanking: ' . ( $this->get_admin_option( 'disable_ranking' ) == '1' ? 'true' : 'false' ) . ' });'; if ( $this->get_admin_option( 'save_scores' ) == '1' ) { @@ -119,7 +122,9 @@ function load_quiz_script() + "" );'; - if ( $this->get_admin_option( 'email_label' ) != '' ) { + if ( $name || $this->get_admin_option( 'email_label' ) != '') { + $display = $name ? 'style=\"display: none;\"' : ''; + $out .= ' // insert a email field before the button $("#slickQuiz' . $quiz->id . ' .buttonWrapper").before( diff --git a/php/slickquiz-helper.php b/php/slickquiz-helper.php index 5c19222..f26a480 100644 --- a/php/slickquiz-helper.php +++ b/php/slickquiz-helper.php @@ -24,31 +24,34 @@ class SlickQuizHelper function get_admin_options() { $this->adminOptions = apply_filters( 'slickquiz_admin_options', array( - 'disabled_quiz_message' => 'Sorry. The requested quiz has been disabled.', - 'missing_quiz_message' => 'Sorry. The requested quiz could not be found.', - 'start_button_text' => 'Get Started!', - 'check_answer_text' => 'Check My Answer!', - 'next_question_text' => 'Next »', - 'back_button_text' => '', - 'try_again_text' => '', - 'your_score_text' => 'Your Score:', - 'your_ranking_text' => 'Your Ranking:', - 'skip_start_button' => '0', - 'number_of_questions' => '', - 'random_sort_questions' => '0', - 'random_sort_answers' => '0', - 'random_sort' => '0', - 'disable_next' => '0', - 'perquestion_responses' => '1', - 'completion_responses' => '0', - 'question_count' => '1', - 'question_number' => '1', - 'save_scores' => '0', - 'name_label' => 'Your Name:', - 'email_label' => '', - 'share_links' => '0', - 'share_message' => 'I\'m a [RANK]! I just scored [SCORE] on the [NAME] quiz!', - 'twitter_account' => '' + 'disabled_quiz_message' => 'Sorry. The requested quiz has been disabled.', + 'missing_quiz_message' => 'Sorry. The requested quiz could not be found.', + 'start_button_text' => 'Get Started!', + 'check_answer_text' => 'Check My Answer!', + 'next_question_text' => 'Next »', + 'back_button_text' => '', + 'try_again_text' => '', + 'your_score_text' => 'Your Score:', + 'your_ranking_text' => 'Your Ranking:', + 'skip_start_button' => '0', + 'number_of_questions' => '', + 'random_sort_questions' => '0', + 'random_sort_answers' => '0', + 'random_sort' => '0', + 'disable_next' => '0', + 'perquestion_responses' => '1', + 'perquestion_response_answers' => '0', + 'completion_responses' => '0', + 'question_count' => '1', + 'question_number' => '1', + 'disable_score' => '0', + 'disable_ranking' => '0', + 'save_scores' => '0', + 'name_label' => 'Your Name:', + 'email_label' => '', + 'share_links' => '0', + 'share_message' => 'I\'m a [RANK]! I just scored [SCORE] on the [NAME] quiz!', + 'twitter_account' => '' ) ); $pluginOptions = get_option( $this->adminOptionsName ); diff --git a/php/slickquiz-new.php b/php/slickquiz-new.php index ea5f733..a69d39d 100644 --- a/php/slickquiz-new.php +++ b/php/slickquiz-new.php @@ -12,6 +12,10 @@ class SlickQuizNew extends SlickQuizModel // Constructor function __construct() { + global $pluginOptions; + + $this->get_admin_options(); + // Add Form JS // wp_enqueue_script( 'tiny_mce' ); // the_editor('', 'quizContent'); @@ -58,3 +62,7 @@ function __construct()

Previewing will save changes to a draft version.

+ + diff --git a/php/slickquiz-options.php b/php/slickquiz-options.php index 0b52914..f97c4c2 100644 --- a/php/slickquiz-options.php +++ b/php/slickquiz-options.php @@ -19,6 +19,11 @@ function __construct() $this->get_admin_options(); if ( isset( $_POST['slickQuizOptions'] ) ) { + // Handle checkboxes + if ( isset( $_POST['slickQuizOptions']['perquestion_response_answers'] ) != true ) { + $_POST['slickQuizOptions']['perquestion_response_answers'] = '0'; + } + $this->adminOptions = array_merge( $this->adminOptions, stripslashes_deep( $_POST['slickQuizOptions'] ) ); update_option( $this->adminOptionsName, $this->adminOptions ); @@ -69,7 +74,7 @@ function show_alert_messages() - @@ -78,7 +83,7 @@ function show_alert_messages() - @@ -202,13 +207,17 @@ function show_alert_messages() - + get_admin_option( 'perquestion_responses' ) == '0' ? print_r('checked="checked"') : ''; ?> /> No   get_admin_option( 'perquestion_responses' ) == '1' ? print_r('checked="checked"') : ''; ?> /> Yes +      get_admin_option( 'perquestion_response_answers' ) == '1' ? print_r('checked="checked"') : ''; ?> /> Also display answer options +
Selecting "Yes" will display result messaging after submitting each answer.
+ When enabled, additionally enabling "Also display answer options" will include the answer options with results.
@@ -220,6 +229,7 @@ function show_alert_messages() get_admin_option( 'completion_responses' ) == '0' ? print_r('checked="checked"') : ''; ?> /> No   get_admin_option( 'completion_responses' ) == '1' ? print_r('checked="checked"') : ''; ?> /> Yes +
Selecting "Yes" will display the answer options along with result messaging at the end of the quiz. @@ -246,6 +256,30 @@ function show_alert_messages() get_admin_option( 'question_number' ) == '1' ? print_r('checked="checked"') : ''; ?> /> Yes + + + + + + get_admin_option( 'disable_score' ) == '0' ? print_r('checked="checked"') : ''; ?> /> No   + get_admin_option( 'disable_score' ) == '1' ? print_r('checked="checked"') : ''; ?> /> Yes +
Note: Scores will still be saved, if "Save user scores?" is enabled. + + + + + + + + get_admin_option( 'disable_ranking' ) == '0' ? print_r('checked="checked"') : ''; ?> /> No   + get_admin_option( 'disable_ranking' ) == '1' ? print_r('checked="checked"') : ''; ?> /> Yes +
Note: If disabled, ranking levels will no longer be required during quiz creation. + + @@ -271,7 +305,8 @@ function show_alert_messages() -
This field will only display if saving scores is enabled and the user is not logged in. +
This field will only display if saving scores is enabled and the user is not logged in.
+ User names will ALWAYS be saved for logged in users if score saving is enabled.
@@ -281,8 +316,8 @@ function show_alert_messages() -
If left blank, no EMAIL field will be displayed and email addresses will NOT be stored.
- This field will only display if saving scores is enabled and the user is not logged in.
+
This field will only display if saving scores is enabled and the user is not logged in.
+ Email addresses will ALWAYS be saved for logged in users if score saving is enabled.
diff --git a/php/slickquiz-preview.php b/php/slickquiz-preview.php index 9516ad2..32926fc 100644 --- a/php/slickquiz-preview.php +++ b/php/slickquiz-preview.php @@ -89,9 +89,12 @@ function get_quiz_json() randomSort: get_admin_option( 'random_sort' ) == '1' ? 'true' : 'false' ) ?>, preventUnanswered: get_admin_option( 'disable_next' ) == '1' ? 'true' : 'false' ) ?>, perQuestionResponseMessaging: get_admin_option( 'perquestion_responses' ) == '1' ? 'true' : 'false' ) ?>, + perQuestionResponseAnswers: get_admin_option( 'perquestion_response_answers' ) == '1' ? 'true' : 'false' ) ?>, completionResponseMessaging: get_admin_option( 'completion_responses' ) == '1' ? 'true' : 'false' ) ?>, displayQuestionCount: get_admin_option( 'question_count' ) == '1' ? 'true' : 'false' ) ?>, - displayQuestionNumber: get_admin_option( 'question_number' ) == '1' ? 'true' : 'false' ) ?> + displayQuestionNumber: get_admin_option( 'question_number' ) == '1' ? 'true' : 'false' ) ?>, + disableScore: get_admin_option( 'disable_score' ) == '1' ? 'true' : 'false' ) ?>, + disableRanking: get_admin_option( 'disable_ranking' ) == '1' ? 'true' : 'false' ) ?> }); }); diff --git a/readme.txt b/readme.txt index 4a66db9..0791548 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: http://www.gofundme.com/slickquiz Tags: quiz, test, jquery, javascript, education, elearning, generator, manager, question, answer, score, rank Requires at least: 3.0 Tested up to: 3.9 -Stable tag: 1.3.2 +Stable tag: 1.3.3 License: GPLv3 or later License URI: http://www.gnu.org/licenses/gpl.html @@ -155,6 +155,13 @@ Also, see the [SlickQuiz Issues](https://github.com/jewlofthelotus/SlickQuiz-Wor == Changelog == += 1.3.3 = +* NEW option for questions with single answers - force the answers to use checkboxes to obscure the fact that there is only one answer from the user. +* NEW option to display answer options along with response messaging after each question. +* NEW option to prevent the score from displaying with the results. +* NEW option to prevent the ranking level from displaying with the results (also makes ranking levels optional in quiz forms). +* Adjusted how email saving works: if score saving is enabled and a user is logged in, an email will always be saved for them (regardless of whether or not there is a value in the Email field is set to display.) + = 1.3.2 = * NEW developer methods for saving extra data along with the quiz via JavaScript and WordPress. See [Installation](http://wordpress.org/plugins/slickquiz/installation/) for more details. Thanks to [@nfreear](https://github.com/nfreear) for the contribution! @@ -306,6 +313,13 @@ This is the initial setup of the plugin. == Upgrade Notice == += 1.3.3 = +* NEW option for questions with single answers - force the answers to use checkboxes to obscure the fact that there is only one answer from the user. +* NEW option to display answer options along with response messaging after each question. +* NEW option to prevent the score from displaying with the results. +* NEW option to prevent the ranking level from displaying with the results (also makes ranking levels optional in quiz forms). +* Adjusted how email saving works: if score saving is enabled and a user is logged in, an email will always be saved for them (regardless of whether or not there is a value in the Email field is set to display.) + = 1.3.2 = * NEW developer methods for saving extra data along with the quiz via JavaScript and WordPress. See [Installation](http://wordpress.org/plugins/slickquiz/installation/) for more details. Thanks to [@nfreear](https://github.com/nfreear) for the contribution! diff --git a/slickquiz.php b/slickquiz.php index c5e2d01..eaffe82 100644 --- a/slickquiz.php +++ b/slickquiz.php @@ -4,7 +4,7 @@ Plugin Name: SlickQuiz Plugin URI: http://github.com/jewlofthelotus/SlickQuiz-WordPress Description: Plugin for displaying and managing pretty, dynamic quizzes. -Version: 1.3.2 +Version: 1.3.3 Author: Julie Cameron Author URI: http://juliecameron.com License: GPLv3 or later diff --git a/slickquiz/README.md b/slickquiz/README.md index 4481a95..3d8d8a5 100644 --- a/slickquiz/README.md +++ b/slickquiz/README.md @@ -16,10 +16,13 @@ To initialize your quiz: }); -### Available Options +## Available Options **`json`** (JSON Object) - your quiz JSON, pass this instead of setting quizJSON outside of the plugin (see js/slickQuiz-config.js) + +#### Text Options + **`checkAnswerText`** (String) *Default: 'Check My Answer!';* - the text to use on the check answer button **`nextQuestionText`** (String) *Default: 'Next »';* - the text to use on the next question button @@ -28,6 +31,19 @@ To initialize your quiz: **`tryAgainText`** (String) *Default: '';* - the text to use on the try again button; if left null / blank - no try again button will be displayed +**`preventUnansweredText`** (String) *Defaut: 'You must select at least one answer.';* - the text to display if a user submits a blank answer while preventUnanswered is enabled + +**`questionCountText`** (String) *Defaut: 'Question %current of %total';* - if displayQuestionCount is enabled, this will format that text using the string provided. %current and %total are placeholders that will output the appropriate values. Note: displayQuestionCount may eventually be deprecated in favor of this option + +**`questionTemplateText`** (String) *Defaut: '%count. %text';* - if displayQuestionNumber is enabled, this will format that question number and question using the string provided. %count and %text are placeholders that will output the appropriate values. Note: displayQuestionNumber may eventually be deprecated in favor of this option + +**`scoreTemplateText`** (String) *Defaut: '%score / %total';* - the format of the final score text. %score and %total are placeholders that will output the appropriate values + +**`nameTemplateText`** (String) *Defaut: '<span>Quiz: </span>%name';* - the format of the quiz name; %name is a placeholder that will output the quiz name. Note: the "Quiz" span in the default value is used to enhance accessibility, it will not display on the screen. + + +#### Functionality Options + **`skipStartButton`** (Boolean) *Default: false;* - whether or not to skip the quiz "start" button **`numberOfQuestions`** (Integer) *Default: null;* - the number of questions to load from the question set in the JSON object, defaults to null (all questions); Note: If you set this to an integer, you'll probably also want to set randomSortQuestions to **true** to ensure that you get a mixed set of questions each page load. @@ -40,21 +56,36 @@ To initialize your quiz: **`perQuestionResponseMessaging`** (Boolean) *Default: true;* - Displays correct / incorrect response messages after each question is submitted. -**`completionResponseMessaging`** (Boolean) *Default: false;* - Displays all questions and selected answers with correct or incorrect response messages when the quiz is completed. +**`perquestionResponseAnswers`** (Boolean) *Default: false;* - Keeps the answer options in display after the question is submitted. Note: this should be used in tandem with perQuestionResponseMessaging + +**`completionResponseMessaging`** (Boolean) *Default: false;* - Displays all questions and answers with correct or incorrect response messages when the quiz is completed. + +**`displayQuestionCount`** (Boolean) *Default: true;* - whether or not to display the number of questions and which question the user is on, for example "Question 3 of 10". Note: this may eventually be deprecated in favor of questionCountText + +**`displayQuestionNumber`** (Boolean) *Default: true;* - whether or not to display the number of the question along side the question itself, for example, the "1." in "1. What is the first letter of the alphabet?" Note: this may eventually be deprecated in favor of questionTemplateText + +**`disableScore`** (Boolean) *Default: false;* - Removes the score from the final results display. Eliminates the need for an element with class quizScore in the markup. + +**`disableRanking`** (Boolean) *Default: false;* - Removes the ranking leve from the final results display. Eliminates the need for an element with class quizLevel in the markup, as well as the need for JSON values for level1 through level5. + + +#### Question Options + +*See "Base Config Options" below for examples* -**`displayQuestionCount`** (Boolean) *Default: true;* - whether or not to display the number of questions and which question the user is on, for example "Question 3 of 10" +**`select_any`** (Boolean) *Optional* - Use if there is more than one true answer and when submitting any single true answer should be considered correct. (Select ANY that apply vs. Select ALL that apply) -**`displayQuestionNumber`** (Boolean) *Default: true;* - whether or not to display the number of the question along side the question itself, for example, the "1." in "1. What is the first letter of the alphabet?" +**`force_checkbox`** (Boolean) *Optional* - Set this to `true` if you want to render checkboxes instead of radios even if the question only has one true answer. -### Event Options +#### Event Options **`events.onStartQuiz`** (function) *Default: empty;* - a function to be executed once the quiz has started. **`events.onCompleteQuiz`** (function) *Default: empty;* - a function to be executed the quiz has completed; the function will be passed two arguments in an object: options.questionCount, options.score -### Animation Callback Options +#### Animation Callback Options **`animationCallbacks.setupQuiz`** (function) *Default: empty;* - a function to be executed once all jQuery animations have completed in the setupQuiz method @@ -136,13 +167,13 @@ See `js/slickQuiz-config.js` {"option": "a correct answer", "correct": true}, {"option": "another correct answer", "correct": true} ], - "select_any": false, // optional, see below "correct": "The Correct Response Message", - "incorrect": "The Incorrect Response Message" + "incorrect": "The Incorrect Response Message", + "select_any": false, // optional, see "Question Options" above + "force_checkbox": false // optional, see "Question Options" above } ] } -Note: `select_any` is used if there is more than one true answer and when submitting any single true answer is considered correct. (Select ANY that apply vs. Select ALL that apply) Created by [Julie Cameron](http://juliecameron.com) while previously employed at [Quicken Loans](http://quickenloans.com), Detroit, MI diff --git a/slickquiz/js/slickQuiz.js b/slickquiz/js/slickQuiz.js index f08da76..3557c56 100644 --- a/slickquiz/js/slickQuiz.js +++ b/slickquiz/js/slickQuiz.js @@ -2,8 +2,8 @@ * SlickQuiz jQuery Plugin * http://github.com/jewlofthelotus/SlickQuiz * - * @updated March 23, 2014 - * @version 1.5.15 + * @updated August 9, 2014 + * @version 1.5.164 * * @author Julie Cameron - http://www.juliecameron.com * @copyright (c) 2013 Quicken Loans - http://www.quickenloans.com @@ -21,15 +21,23 @@ nextQuestionText: 'Next »', backButtonText: '', tryAgainText: '', + questionCountText: 'Question %current of %total', + preventUnansweredText: 'You must select at least one answer.', + questionTemplateText: '%count. %text', + scoreTemplateText: '%score / %total', + nameTemplateText: 'Quiz: %name', skipStartButton: false, numberOfQuestions: null, randomSortQuestions: false, randomSortAnswers: false, preventUnanswered: false, + disableScore: false, + disableRanking: false, perQuestionResponseMessaging: true, + perQuestionResponseAnswers: false, completionResponseMessaging: false, - displayQuestionCount: true, - displayQuestionNumber: true, + displayQuestionCount: true, // Deprecate? + displayQuestionNumber: true, // Deprecate? animationCallbacks: { // only for the methods that have jQuery animations offering callback setupQuiz: function () {}, startQuiz: function () {}, @@ -52,10 +60,12 @@ answersClass = 'answers', responsesClass = 'responses', correctClass = 'correctResponse', + incorrectClass = 'incorrectResponse', correctResponseClass = 'correct', incorrectResponseClass = 'incorrect', checkAnswerClass = 'checkAnswer', nextQuestionClass = 'nextQuestion', + lastQuestionClass = 'lastQuestion', backToQuestionClass = 'backToQuestion', tryAgainClass = 'tryAgain', @@ -64,7 +74,9 @@ _questions = '.' + questionGroupClass, _question = '.' + questionClass, _answers = '.' + answersClass, + _answer = '.' + answersClass + ' li', _responses = '.' + responsesClass, + _response = '.' + responsesClass + ' li', _correct = '.' + correctClass, _correctResponse = '.' + correctResponseClass, _incorrectResponse = '.' + incorrectResponseClass, @@ -155,31 +167,30 @@ // 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= 1 - var key = []; - for (i=0; i= 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 - }; - } + // 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 = { @@ -190,7 +201,8 @@ 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(quizValues.info.name).fadeIn(1000, kN(key,1)); + $quizName.hide().html(plugin.config.nameTemplateText + .replace('%name', quizValues.info.name) ).fadeIn(1000, kN(key,1)); $quizHeader.hide().prepend($('
' + quizValues.info.main + '
')).fadeIn(1000, kN(key,2)); $quizResultsCopy.append(quizValues.info.results); @@ -211,14 +223,21 @@ var questionHTML = $('
  • '); if (plugin.config.displayQuestionCount) { - questionHTML.append('
    Question ' + count + ' of ' + questionCount + '
    '); + questionHTML.append('
    ' + + plugin.config.questionCountText + .replace('%current', '' + count + '') + .replace('%total', '' + + questionCount + '') + '
    '); } - var questionNumber = ''; + var formatQuestion = ''; if (plugin.config.displayQuestionNumber) { - questionNumber += count + '. '; + formatQuestion = plugin.config.questionTemplateText + .replace('%count', count).replace('%text', question.q); + } else { + formatQuestion = question.q; } - questionHTML.append('

    ' + questionNumber + question.q + '

    '); + questionHTML.append('

    ' + formatQuestion + '

    '); // Count the number of true values var truths = 0; @@ -240,9 +259,15 @@ question.a; // prepare a name for the answer inputs based on the question - var selectAny = question.select_any ? question.select_any : false, - inputName = 'question' + (count - 1), - inputType = (truths > 1 && !selectAny ? 'checkbox' : 'radio'); + var selectAny = question.select_any ? question.select_any : false, + forceCheckbox = question.force_checkbox ? question.force_checkbox : false, + checkbox = (truths > 1 && !selectAny) || forceCheckbox, + inputName = 'question' + (count - 1), + inputType = checkbox ? 'checkbox' : 'radio'; + + if( count == quizValues.questions.length ) { + nextQuestionClass = nextQuestionClass + ' ' + lastQuestionClass; + } for (i in answers) { if (answers.hasOwnProperty(i)) { @@ -354,12 +379,12 @@ $(_element + ' input').prop('checked', false).prop('disabled', false); $quizLevel.attr('class', 'quizLevel'); - $(_element + ' ' + _correct).removeClass(correctClass); + $(_element + ' ' + _question).removeClass(correctClass).removeClass(incorrectClass); + $(_element + ' ' + _answer).removeClass(correctResponseClass).removeClass(incorrectResponseClass); $(_element + ' ' + _question + ',' + _element + ' ' + _responses + ',' + - _element + ' ' + _correctResponse + ',' + - _element + ' ' + _incorrectResponse + ',' + + _element + ' ' + _response + ',' + _element + ' ' + _nextQuestionBtn + ',' + _element + ' ' + _prevQuestionBtn ).hide(); @@ -387,35 +412,43 @@ kN = keyNotch; // you specify the notch, you get a callback function for your animation var questionLI = $($(checkButton).parents(_question)[0]), - answerInputs = questionLI.find('input:checked'), + 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]; + var answer = answers[i], + index = parseInt(i, 10); if (answer.correct) { - trueAnswers.push(parseInt(i, 10)); + 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 = []; - answerInputs.each( function() { + 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('You must select at least one answer.'); + alert(plugin.config.preventUnansweredText); return false; } @@ -424,6 +457,8 @@ if (correctResponse) { questionLI.addClass(correctClass); + } else { + questionLI.addClass(incorrectClass); } // Toggle appropriate response (either for display now and / or on completion) @@ -432,7 +467,9 @@ // If perQuestionResponseMessaging is enabled, toggle response and navigation now if (plugin.config.perQuestionResponseMessaging) { $(checkButton).hide(); - questionLI.find(_answers).hide(); + if (!plugin.config.perQuestionResponseAnswers) { + questionLI.find(_answers).hide(); + } questionLI.find(_responses).show(); questionLI.find(_nextQuestionBtn).fadeIn(300, kN(key,1)); questionLI.find(_prevQuestionBtn).fadeIn(300, kN(key,2)); @@ -490,7 +527,7 @@ var prevQuestion = questionLI.prev(_question); questionLI.fadeOut(300, function() { - prevQuestion.removeClass(correctClass); + prevQuestion.removeClass(correctClass).removeClass(incorrectClass); prevQuestion.find(_responses + ', ' + _responses + ' li').hide(); prevQuestion.find(_answers).show(); prevQuestion.find(_checkAnswerBtn).show(); @@ -508,7 +545,7 @@ // Back to question from responses } else { questionLI.find(_responses).fadeOut(300, function(){ - questionLI.removeClass(correctClass); + questionLI.removeClass(correctClass).removeClass(incorrectClass); questionLI.find(_responses + ' li').hide(); 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)); @@ -533,20 +570,31 @@ 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 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% - ], - score = $(_element + ' ' + _correct).length, - levelRank = plugin.method.calculateLevel(score), - levelText = $.isNumeric(levelRank) ? levels[levelRank] : ''; - - $(_quizScore + ' span').html(score + ' / ' + questionCount); - $(_quizLevel + ' span').html(levelText); - $(_quizLevel).addClass('level' + levelRank); + var score = $(_element + ' ' + _correct).length; + + if (plugin.config.disableScore) { + $(_quizScore).remove() + } else { + $(_quizScore + ' span').html(plugin.config.scoreTemplateText + .replace('%score', score).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 @@ -643,6 +691,17 @@ 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();