├── .gitignore
├── README.md
├── book
├── script.js
└── style.css
├── index.js
├── package.json
└── screenshot.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gitbook-plugin-quiz
2 |
3 |
4 |
5 | Allows to insert simple quizes into gitbook.
6 |
7 | 
8 |
9 | ### How to use?
10 |
11 | Add plugin to your `book.json`, then run `gitbook install`:
12 |
13 | ```
14 | {
15 | "plugins": ["quiz"]
16 | }
17 | ```
18 |
19 | It is possible to configure generic labels and text:
20 |
21 | ```
22 | "pluginsConfig": {
23 | "quiz": {
24 | "labels": {
25 | "check" : "Check my answers",
26 | "showExplanation" : "Explain",
27 | "showCorrect" : "Show all",
28 | "explanationHeader" : "Explanation"
29 | },
30 | "text": {
31 | "noChosen" : "Choose at least one answer",
32 | "incomplete" : "Some correct answers are missing"
33 | },
34 | "buttons": {
35 | "showCorrect" : true,
36 | "showExplanation" : true
37 | }
38 | }
39 | }
40 | ```
41 |
42 | Add quiz markup in your gitbook:
43 |
44 | ```
45 |
46 |
47 | What is gitbook used for?
48 | To read books
49 | To book hotel named git
50 | To write and publish beautiful books
51 | GitBook.com lets you write, publish and manage your books online as a service.
52 |
53 |
54 | Is it quiz?
55 | Yes
56 | No
57 |
58 |
59 | This is multiple dropdown quiz, in each dropdown select a correct number corresponding to the dropdown's order
60 |
61 | First
62 | Second
63 | Third
64 | Fourth
65 |
66 |
67 | First
68 | Second
69 | Third
70 | Fourth
71 |
72 |
73 | First
74 | Second
75 | Third
76 | Fourth
77 |
78 |
79 | First
80 | Second
81 | Third
82 | Fourth
83 |
84 |
85 |
86 | ```
87 |
--------------------------------------------------------------------------------
/book/script.js:
--------------------------------------------------------------------------------
1 | require(["jquery", "gitbook"], function($, gitbook) {
2 | var configuration = {"labels": {}, text: {}, "buttons": {"showCorrect": true, "showExplanation": true}};
3 | var isStarted = false;
4 | /**
5 | * Enumeration for default labels and text
6 | * @type {Object}
7 | */
8 | var LABELS_AND_TEXT_ENUM = {
9 | "labels": {
10 | "showCorrect" : "Show correct answers",
11 | "check" : "Check",
12 | "showExplanation" : "Show explanation",
13 | "explanationHeader" : ""
14 | },
15 | "text": {
16 | "noChosen" : "No answer is chosen",
17 | "incomplete" : "Some correct answers missing",
18 | "rightAnswer" : "Right answer!",
19 | "wrongAnswer" : "Wrong answer!"
20 | }
21 | };
22 | /**
23 | * ENumeration for button actions
24 | *
25 | * @type {{CHECK: string, EXPLAIN: string, SHOW: string}}
26 | */
27 | var BUTTON_ACTIONS_ENUM = {
28 | CHECK : "check",
29 | EXPLAIN : "explain",
30 | SHOW : "show"
31 | };
32 | /**
33 | * Generates random string of given length
34 | *
35 | * @param {Number} [length]
36 | * @returns {string}
37 | */
38 | function randomString(length) {
39 | var CHARS;
40 |
41 | if (length === undefined) {
42 | length = 20;
43 | }
44 | CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
45 | var result = "";
46 | for (var i = length; i > 0; --i) {
47 | result += CHARS[Math.round(Math.random() * (CHARS.length - 1))];
48 | }
49 | return result;
50 | }
51 | /**
52 | * Asserts value is a String object
53 | *
54 | * @param {String} [value]
55 | * @returns {Bool}
56 | */
57 | function isString(value) {
58 | return typeof value == 'string' ||
59 | (!$.isArray(value) && (!!value && typeof value == 'object') && Object.prototype.toString.call(value) == '[object String]');
60 | }
61 |
62 | /**
63 | * Template for the whole question
64 | *
65 | * @param {jQuery} $el
66 | * @returns {String}
67 | */
68 | var questionTemplate = function ($el) {
69 | var html, question, multiple, $answers, $explanation, name, withExplanation;
70 |
71 | html = "";
72 |
73 | question = $el.find("p").first().html();
74 | multiple = isString($el.attr("multiple"));
75 | $answers = $el.find("answer");
76 | $explanation = $el.find("explanation");
77 |
78 | html += "";
79 | html += "
" + question + "
";
80 |
81 | name = randomString();
82 | $answers.each(function () {
83 | var $answer;
84 |
85 | $answer = $(this);
86 | html += answerTemplate($answer, multiple, name);
87 | });
88 | withExplanation = $explanation.length > 0 && configuration.buttons.showExplanation === true;
89 | html += withExplanation ? explanationTemplate($explanation.html()) : "";
90 | html += messageTemplate();
91 | html += checkButtonTemplate(withExplanation);
92 | html += "
";
93 | return html;
94 | };
95 | /**
96 | * Generates html string for the check button
97 | *
98 | * @param {Boolean} withExplanation
99 | * @returns {String}
100 | */
101 | var checkButtonTemplate = function (withExplanation) {
102 | var checkButton, showExplanationButton, showCorrectAnswers, html;
103 |
104 | html = "";
105 |
106 | checkButton = "" +
107 | configuration.labels.check +
108 | " ";
109 | showExplanationButton = "" +
110 | configuration.labels.showExplanation +
111 | " ";
112 | showCorrectAnswers = "" +
113 | configuration.labels.showCorrect +
114 | " ";
115 |
116 | html += "" +
117 | checkButton +
118 | (configuration.buttons.showExplanation && withExplanation ? showExplanationButton : "") +
119 | (configuration.buttons.showCorrect ? showCorrectAnswers : "") +
120 | "
";
121 |
122 | return html;
123 | };
124 | /**
125 | * Generates html string for the answer
126 | *
127 | * @param {jQuery} $el
128 | * @param {Boolean} multiple
129 | * @param {String} name
130 | * @returns {String}
131 | */
132 | var answerTemplate = function ($el, multiple, name) {
133 | var correct, text, html, $options;
134 |
135 | correct = isString($el.attr("correct"));
136 | text = $el.text();
137 | html = $el.html();
138 | $options = $el.find("option");
139 | if ($options.length > 0) {
140 | var result;
141 |
142 | result = "";
143 | $.each($options, function (i, option) {
144 | var $option = $(option);
145 | result += "" + $option.text() + " ";
146 | });
147 | result += " " + "
";
148 | return result;
149 | } else if (multiple) {
150 | return "" +
151 | " " + html +
152 | " " +
153 | "
";
154 | } else {
155 | return "" +
156 | " " + html +
157 | " " +
158 | "
";
159 | }
160 | };
161 | /**
162 | * Generates html string for the message for explanation
163 | *
164 | * @param {String} text
165 | */
166 | var explanationTemplate = function (text) {
167 | return "" +
168 | (configuration.labels.explanationHeader && configuration.labels.explanationHeader.length > 0 ?
169 | "" : "") +
170 | text +
171 | "
";
172 | };
173 | /**
174 | * Generates html string for the message for the answer
175 | */
176 | var messageTemplate = function () {
177 | return "
";
178 | };
179 | /**
180 | * Analyzes answers to questions and takes action
181 | *
182 | * @param {jQuery} $question the question element to analyze
183 | * @param {Number} numberOfCorrect the number of correct answers
184 | * @param {Number} totalNumberOfAnswers total number of existing answers to question
185 | * @param {jQuery} $correctChecked correct answers checked
186 | * @param {jQuery} $incorrectChecked incorrect answers checked
187 | */
188 | var analyseAnswers = function ($question, numberOfCorrect, totalNumberOfAnswers, $correctChecked, $incorrectChecked) {
189 | var numberIncorrectChecked, numberCorrectChecked;
190 |
191 | numberCorrectChecked = $correctChecked.length;
192 | numberIncorrectChecked = $incorrectChecked.length;
193 |
194 | //no selected
195 | if (numberCorrectChecked === 0 && numberIncorrectChecked === 0) {
196 | noAnswerChecked($question);
197 | return;
198 | }
199 | //mark answers
200 | markAnswersAsCorrectOrIncorrect($correctChecked, $incorrectChecked);
201 | //if incomplete
202 | if ($correctChecked.length < numberOfCorrect && $incorrectChecked.length === 0) {
203 | handleIncomplete($question);
204 | return;
205 | }
206 | //if correct
207 | if ($correctChecked.length === numberOfCorrect && $incorrectChecked.length === 0) {
208 | showExplanation($question);
209 | return;
210 | }
211 | };
212 | /**
213 | * Handles correct state of the question
214 | *
215 | * @param {jQuery} $question
216 | */
217 | var handleCorrect = function ($question) {
218 | var $message, $explanation;
219 |
220 | $message = getMessageElement($question);
221 | $explanation = getExplanationElement($question);
222 | $message.text(configuration.text.rightAnswer).removeClass("hidden");
223 | $explanation.removeClass("hidden");
224 | };
225 | /**
226 | * Handles incorrect state of the question
227 | *
228 | * @param {jQuery} $question
229 | */
230 | var handleIncorrect = function ($question) {
231 | var $message;
232 |
233 | $message = getMessageElement($question);
234 | $message.text(configuration.text.wrongAnswer).removeClass("hidden");
235 | };
236 | /**
237 | * Handles incorrect state of the question
238 | *
239 | * @param {jQuery} $question
240 | */
241 | var handleIncomplete = function ($question) {
242 | var $message;
243 |
244 | $message = getMessageElement($question);
245 | $message.text(configuration.text.incomplete).removeClass("hidden").addClass("error");
246 | };
247 | /**
248 | * Action to take when no answer is checked in the question
249 | *
250 | * @param {jQuery} $question
251 | */
252 | var noAnswerChecked = function ($question) {
253 | var $message;
254 |
255 | $message = $question.find(".quiz-question-message");
256 | $message.show().text(configuration.text.noChosen).addClass("error");
257 | };
258 | /**
259 | * Marks answers with corresponding classes
260 | *
261 | * @param {jQuery} $correctChecked
262 | * @param {jQuery} [$incorrectChecked]
263 | */
264 | var markAnswersAsCorrectOrIncorrect = function ($correctChecked, $incorrectChecked) {
265 | $correctChecked.each(function () {
266 | $(this).closest(".quiz-answer").find(".quiz-answer-check").addClass("correct");
267 | });
268 | if ($incorrectChecked) {
269 | $incorrectChecked.each(function () {
270 | $(this).closest(".quiz-answer").find(".quiz-answer-check").addClass("incorrect");
271 | });
272 | }
273 | };
274 | /**
275 | * Returns all answers elements of the question
276 | *
277 | * @param {jQuery} $question
278 | * @returns {jQuery}
279 | */
280 | var getAllAnswersElements = function ($question) {
281 | return $question.find(".quiz-answer");
282 | };
283 | /**
284 | * Returns message element of the question
285 | *
286 | * @param {jQuery} $question
287 | * @returns {jQuery}
288 | */
289 | var getMessageElement = function ($question) {
290 | return $question.find(".quiz-question-message");
291 | };
292 | /**
293 | * Returns explanation element of the question
294 | *
295 | * @param {jQuery} $question
296 | * @returns {jQuery}
297 | */
298 | var getExplanationElement = function ($question) {
299 | return $question.find(".quiz-question-explanation");
300 | };
301 | /**
302 | * Clears question state - clears messages and correct/incorrect states
303 | *
304 | * @param {jQuery} $question question element to clear
305 | */
306 | var clearQuestionState = function ($question) {
307 | var $answers, $message, $explanation;
308 |
309 | $answers = getAllAnswersElements($question);
310 | $message = getMessageElement($question);
311 | $explanation = getExplanationElement($question);
312 | $answers.each(function () {
313 | $(this).find(".quiz-answer-check").removeClass("correct").removeClass("incorrect");
314 | });
315 | $message.text("").addClass("hidden");
316 | $explanation.addClass("hidden");
317 | };
318 | /**
319 | * Handles click on all buttons
320 | * @param {Event} ev
321 | */
322 | var onClickButton = function (ev) {
323 | var $target, $question, action;
324 |
325 | ev.preventDefault();
326 | ev.stopPropagation();
327 | $target = $(ev.currentTarget);
328 | $question = $target.closest(".quiz-question");
329 | action = $target.data("action");
330 |
331 | switch (action) {
332 | case BUTTON_ACTIONS_ENUM.CHECK:
333 | clearQuestionState($question);
334 | checkAnswers($question);
335 | break;
336 | case BUTTON_ACTIONS_ENUM.EXPLAIN:
337 | showExplanation($question);
338 | break;
339 | case BUTTON_ACTIONS_ENUM.SHOW:
340 | clearQuestionState($question);
341 | showCorrect($question);
342 | break;
343 | default:
344 | checkAnswers($question);
345 | }
346 | };
347 | /**
348 | * Shows an explanation
349 | *
350 | * @param {jQuery} $question
351 | */
352 | var showExplanation = function ($question) {
353 | var $explanation;
354 |
355 | $explanation = getExplanationElement($question);
356 | $explanation.removeClass("hidden");
357 | };
358 | /**
359 | * Shows correct answers
360 | *
361 | * @param {jQuery} $question
362 | */
363 | var showCorrect = function ($question) {
364 | var $correct;
365 |
366 | $correct = $question.find(".quiz-answer input[data-correct=true]");
367 | if ($correct.length === 0) {
368 | $correct = $question.find(".quiz-answer option[data-correct=true]");
369 | }
370 | $correct.prop("checked", true).prop("selected", true);
371 | markAnswersAsCorrectOrIncorrect($correct);
372 | };
373 | /**
374 | * Handles check button click
375 | */
376 | var checkAnswers = function ($question) {
377 | var numberOfCorrect, totalNumberOfAnswers, $incorrectChecked, $correctChecked;
378 |
379 | numberOfCorrect = $question.find(".quiz-answer input[data-correct=true]").length;
380 | totalNumberOfAnswers = $question.find(".quiz-answer").length;
381 | $correctChecked = $question.find(".quiz-answer input[data-correct=true]:checked");
382 | if ($correctChecked.length === 0) {
383 | $correctChecked = $question.find(".quiz-answer select option[data-correct=true]:selected");
384 | }
385 | $incorrectChecked = $question.find(".quiz-answer input[data-correct=false]:checked");
386 | if ($incorrectChecked.length === 0) {
387 | $incorrectChecked = $question.find(".quiz-answer select option[data-correct=false]:selected");
388 | }
389 |
390 | analyseAnswers($question, numberOfCorrect, totalNumberOfAnswers, $correctChecked, $incorrectChecked);
391 | };
392 | /**
393 | * Transforms quiz element into appropriate markup
394 | *
395 | * @param {jQuery} $quiz
396 | */
397 | var prepareQuiz = function ($quiz) {
398 | var name, $questions, html;
399 |
400 | if ($quiz.data("prepared")) {
401 | return;
402 | }
403 | // Create HTML for the quiz
404 | html = "";
405 | name = $quiz.prop("name") || $quiz.attr("name") || "";
406 | $questions = $quiz.find("question");
407 |
408 | html += "";
409 | html += "
" + name + "
";
410 | $questions.each(function () {
411 | html += questionTemplate($(this));
412 | });
413 | html += "
";
414 | $quiz.html(html);
415 | $quiz.data("prepared", true);
416 | // Assign events
417 | $quiz.find(".buttons .btn").click(onClickButton);
418 | };
419 | /**
420 | * Starting point
421 | */
422 | var init = function () {
423 | if (!isStarted) {
424 | return;
425 | }
426 | $("quiz").each(function () {
427 | prepareQuiz($(this));
428 | });
429 | };
430 |
431 | configuration = $.extend(true, configuration, LABELS_AND_TEXT_ENUM, {});
432 |
433 | gitbook.events.bind("start", function (e, config) {
434 | isStarted = true;
435 | config = config.quiz || {};
436 | configuration = $.extend(true, configuration, LABELS_AND_TEXT_ENUM, config);
437 | init();
438 | });
439 |
440 | gitbook.events.bind("page.change", init);
441 | });
--------------------------------------------------------------------------------
/book/style.css:
--------------------------------------------------------------------------------
1 | .quiz {
2 | padding-left: 10px;
3 | margin-bottom: 20px;
4 | }
5 | /* QUIZ NAME */
6 | .quiz-name {
7 | text-align: center;
8 | font-weight: bold;
9 | font-size: 1.1em;
10 | }
11 | /* QUIZ QUESTION */
12 | .quiz-question {
13 | font-weight: 600;
14 | color: #3c3c3c;
15 | border-bottom: 1px solid #E7EAED;
16 | padding-bottom: 15px;
17 | }
18 | .quiz-question-text {
19 | font-weight: 600;
20 | color: #3C3C3C;
21 | }
22 | .book.color-theme-1 .quiz-question-text{
23 | color: #704214;
24 | border-bottom:1px solid #704214;
25 | }
26 | .book.color-theme-2 .quiz-question-text{
27 | color: #bdcadb;
28 | border-bottom:1px solid #bdcadb;
29 | }
30 | /* ANSWER */
31 | .quiz-answer {
32 | margin-left: 20px;
33 | color: #3c3c3c;
34 | font-weight: 400;
35 | }
36 | .quiz-answer select {
37 | width: 58%;
38 | }
39 | .book.color-theme-1 .quiz-answer {
40 | color: #704214;
41 | }
42 | .book.color-theme-2 .quiz-answer {
43 | color: #bdcadb;
44 | }
45 | .quiz-answer input {
46 | margin-right: 10px;
47 | }
48 | .quiz-answer label {
49 | font-weight: normal;
50 | }
51 | /* EXPLANATION */
52 | .quiz-question-explanation {
53 | font-size: 1.6rem;
54 | margin: 20px 0;
55 | border: 1px solid #ccc;
56 | padding: 9px 15px 20px;
57 | background: #fbfbfb;
58 | box-shadow: inset 0 0 0 1px #eee;
59 | border-radius: 3px;
60 | }
61 | .quiz-question-explanation-header {
62 | color: #aaa;
63 | text-transform: uppercase;
64 | font-weight: bold;
65 | font-style: normal;
66 | margin-bottom: 18px;
67 | }
68 | .book.color-theme-1 .quiz-question-explanation {
69 | border-color: #704214;
70 | background-color: #F3EACB;
71 | color: #704214;
72 | }
73 | .book.color-theme-1 .quiz-question-explanation-header {
74 | color: #8e5916;
75 | }
76 | .book.color-theme-2 .quiz-question-explanation {
77 | border-color: #2c3043;
78 | background-color: #1c1f2b;
79 | color: #BDCADB;
80 | }
81 | .book.color-theme-2 .quiz-question-explanation-header {
82 | color: #E7F2FF;
83 | }
84 | /* MESSAGE */
85 | .quiz-question-message {
86 | font-size: 0.8em;
87 | }
88 | .hidden {
89 | display: none;
90 | }
91 | .correct {
92 | color: green;
93 | }
94 | .incorrect, .error {
95 | color: red;
96 | }
97 | .correct:before {
98 | content: " \2714"
99 | }
100 | .incorrect:before {
101 | content: " \2718"
102 | }
103 | /* buttons */
104 | .quiz .buttons .btn, .quiz .quiz-answer select {
105 | background-color: #fff;
106 | border-radius: 4px;
107 | margin-top: 5px;
108 | margin-bottom: 5px;
109 | display: inline-block;
110 | padding: 6px 12px;
111 | font-size: 14px;
112 | font-weight: 400;
113 | line-height: 1.42857143;
114 | text-align: center;
115 | white-space: nowrap;
116 | vertical-align: middle;
117 | cursor: pointer;
118 | border: 1px solid #ccc;
119 | margin-right: 10px;
120 | }
121 | .quiz .buttons .btn:hover, .quiz .buttons .btn:active {
122 | color: #333;
123 | background-color: #e6e6e6;
124 | border-color: #adadad;
125 | }
126 | .quiz .buttons .btn:active {
127 | outline: 0;
128 | box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
129 | }
130 | /* buttons for theme 1 */
131 | .book.color-theme-1 .quiz .buttons .btn {
132 | background-color: #F3EACB;
133 | color: #704214;
134 | border-color: #704214;
135 | }
136 | .book.color-theme-1 .quiz .buttons .btn:hover, .quiz .buttons .btn:active {
137 | color: #F3EACB;
138 | background-color: #704214;
139 | }
140 | .book.color-theme-1 .quiz .buttons .btn:active {
141 | outline: 0;
142 | box-shadow: inset 0 3px 5px rgba(103, 59, 19, 0.12);
143 | }
144 | /* buttons for theme 2 */
145 | .book.color-theme-2 .quiz .buttons .btn {
146 | color: #BDCADB;
147 | background-color: transparent;
148 | }
149 | .book.color-theme-2 .quiz .buttons .btn:hover, .quiz .buttons .btn:active {
150 | color: #1C1F2B;
151 | background-color: #BDCADB;
152 | }
153 | .book.color-theme-2 .quiz .buttons .btn:active {
154 | outline: 0;
155 | box-shadow: inset 0 3px 5px rgba(80, 86, 100, 0.12);
156 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | website: {
3 | assets: "./book",
4 | js: [
5 | "script.js"
6 | ],
7 | css: [
8 | "style.css"
9 | ]
10 | }
11 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitbook-plugin-quiz",
3 | "version": "0.3.0",
4 | "description": "Allows to insert simple quizes into gitbook",
5 | "keywords": [
6 | "quiz",
7 | "gitbook"
8 | ],
9 | "author": {
10 | "name": "Olga Filipova",
11 | "email": "chudaol@gmail.com"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/chudaol/gitbook-plugin-quiz"
16 | },
17 | "engines": {
18 | "gitbook": ">1.x.x"
19 | },
20 | "dependencies": {
21 | "jquery": "^2.1.4",
22 | "underscore": "^1.8.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chudaol/gitbook-plugin-quiz/ac979c009908e4df3cf34fe66b6fc76bbecaf1b5/screenshot.png
--------------------------------------------------------------------------------