diff --git a/css/img/mark-fail.png b/css/img/mark-fail.png new file mode 100644 index 0000000..cbd3936 Binary files /dev/null and b/css/img/mark-fail.png differ diff --git a/css/img/mark-pass.png b/css/img/mark-pass.png new file mode 100644 index 0000000..cceccd3 Binary files /dev/null and b/css/img/mark-pass.png differ diff --git a/css/questionset.css b/css/questionset.css new file mode 100644 index 0000000..0fe886e --- /dev/null +++ b/css/questionset.css @@ -0,0 +1,111 @@ +.dots-container { + text-align: center; +} +.progress-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + border: 1px solid #494949; + margin: 0 3px; + cursor: pointer; +} +.progress-dot.answered { + background: #494949; +} +.progress-dot.current { + box-shadow: 1px 1px 4px #494949; + margin: -1px 4px 1px 2px; +} +.intro-page { + /*background-color: rgba(255,255,255,0.9);*/ + /*padding: 10px;*/ + position: absolute; + z-index: 20; + height: 100%; + width: 100%; + left: 0px; + top: 0px; +} +.intro-page .title { + font-size: 200%; + font-weight: bold; + text-align: center; + margin: 1em; +} +.intro-page .introduction { + font-size: 125%; + margin: 1em 10%; +} +.intro-page .buttons { + position: absolute; + top: 200px; + left: 182px; + text-align: right; +} + +#qs-startbutton { + border: 4px solid #BABABA; + border-radius: 10px 10px 10px 10px; + font-size: 50px; + padding: 10px 20px; + cursor: pointer; +} + +.qs-progress { + line-height: 40px; + padding: 0; +} + +.questionset-results { + background-image: url(img/Paper-clean.png); + background-color: rgba(255, 255, 255, 0.75); + + padding: 10px; + height: 480px; + text-align: center; +} + +.questionset-results .greeting { + margin-top: 180px; +} + +.questionset-results .score { + font-size: 25px; + margin-top: 40px; +} + +.questionset-results .resulttext { + font-size: 20px; + line-height: 25px; + margin: 40px 100px; + padding-left: 60px; + min-height: 60px; + background-repeat: no-repeat; + background-position: left center; +} + +.questionset-results .resulttext.fail { + background-image: url(img/mark-fail.png); +} +.questionset-results .resulttext.fail em { + font-style: normal; + color: #b9272d; +} + +.questionset-results .resulttext.success { + background-image: url(img/mark-pass.png); +} +.questionset-results .resulttext.success em { + font-style: normal; + color: #6aa81b; +} +.questionset-results .buttons { + position: absolute; + bottom: 40px; + right: 25px; +} + +button, .button { + cursor: pointer; +} \ No newline at end of file diff --git a/js/questionset.js b/js/questionset.js new file mode 100644 index 0000000..078d1da --- /dev/null +++ b/js/questionset.js @@ -0,0 +1,303 @@ +// Will render a Question with multiple choices for answers. + +// Options format: +// { +// title: "Optional title for question box", +// question: "Question text", +// answers: [{text: "Answer text", correct: false}, ...], +// singleAnswer: true, // or false, will change rendered output slightly. +// } +// +// Events provided: +// - h5pQuestionSetFinished: Triggered when a question is finished. (User presses Finish-button) +var H5P = H5P || {}; + +H5P.QuestionSet = function (options, contentId) { + if ( !(this instanceof H5P.QuestionSet) ) + return new H5P.QuestionSet(options, contentId); + + var $ = H5P.jQuery; + var cp = H5P.getContentPath(contentId); + + var texttemplate = '' + +'<% if (introPage.showIntroPage) { %>' + +'
' + +'
<%= introPage.title %>
' + +'
<%= introPage.introduction %>
' + +'
<%= introPage.startButtonText %>
' + +'
' + +'<%} %>' + +'' + + ''; + var resulttemplate = '' + +'
' + +'
<%= greeting %>
' + +'
<%= score %>
' + +'
<%= resulttext %>
' + +'
<%= finishButtonText %>
' + +'
' + + ''; + + var that = this; + var defaults = { + title: "", + randomOrder: false, + initialQuestion: 0, + backgroundImage: "", + progressType: 'dots', + passPercentage: 50, + questions: [], + introPage: { + showIntroPage: true, + title: "Welcome", + introduction: "Click start to start.", + startButtonText: "Start" + }, + texts: { + prevButton: "Previous", + nextButton: "Next", + finishButton: "Finish", + textualProgress: "Question: @current of @total questions" + }, + endGame: { + showResultPage: true, + resultPage: { + successGreeting: "Congratulations!", + successComment: "You have enough correct answers to pass the test.", + failGreeting: "Sorry!", + failComment: "You don't have enough correct answers to pass this test.", + scoreString: "@score/@total", + finishButtonText: "Finish" + }, + animations: { + showAnimations: false, + successVideo: undefined, + failVideo: undefined + } + } + }; + + var template = new EJS({text: texttemplate}); + var endTemplate = new EJS({text: resulttemplate}); + var params = $.extend({}, defaults, options); + + var currentQuestion = 0; + var questionInstances = new Array(); + var allQuestionsAnswered = false; + var $myDom; + + if (params.randomOrder) { + // TODO: Randomize order of questions + console.log("TODO: Randomize order of questions"); + } + + // Instantiate question instances + for (var i=0; i= 0; i--) { + answered = answered && (questionInstances[i]).getAnswerGiven(); + } + + if (currentQuestion === 0) { + $('.prev.button', $myDom).hide(); + } else { + $('.prev.button', $myDom).show(); + } + if (currentQuestion == (params.questions.length - 1)) { + $('.next.button', $myDom).hide(); + if (answered) { + $('.finish.button', $myDom).show(); + } + } else { + $('.next.button', $myDom).show(); + $('.finish.button', $myDom).hide(); + } + + }; + + var _showQuestion = function (questionNumber) { + // Sanitize input. + if (questionNumber < 0) { questionNumber = 0; } + if (questionNumber >= params.questions.length) { questionNumber = params.questions.length - 1; } + + // Hide all questions + $('.question-container', $myDom).hide(); + + // Reshow the requested question + $('#q-' + questionNumber, $myDom).show(); + + // Update progress indicator + // Test if current has been answered. + if (params.progressType == 'textual') { + $('.progress-text', $myDom).text(params.texts.textualProgress.replace("@current", questionNumber+1).replace("@total", params.questions.length)); + } else { + // Set currentNess + $('.progress-dot.current', $myDom).removeClass('current'); + $('#qdot-' + questionNumber, $myDom).addClass('current'); + } + + // Remember where we are + currentQuestion = questionNumber; + _updateButtons(); + return currentQuestion; + }; + + var _displayEndGame = function () { + // Get total score. + var finals = getScore(); + var totals = totalScore(); + var scoreString = params.endGame.resultPage.scoreString.replace("@score", finals).replace("@total", totals); + var success = ((100 * finals / totals) >= params.passPercentage); + var eventData = { + score: scoreString, + passed: success + }; + var displayResults = function () { + if (!params.endGame.showResultPage) { + $(returnObject).trigger('h5pQuestionSetFinished', eventData); + return; + } + + var eparams = { + greeting: (success ? params.endGame.resultPage.succesGreeting : params.endGame.resultPage.failGreeting), + score: scoreString, + scoreclass: (success ? 'success' : 'fail'), + resulttext: (success ? params.endGame.resultPage.successComment : params.endGame.resultPage.failComment), + finishButtonText: params.endGame.resultPage.finishButtonText + }; + + // Show result page. + $myDom.children().hide(); + $myDom.append(endTemplate.render(eparams)); + $('.qs-finishbutton').click(function (ev) { + $(returnObject).trigger('h5pQuestionSetFinished', eventData); + }); + }; + + if (params.endGame.animations.showAnimations) { + var videoData = success ? params.endGame.animations.successVideo : params.endGame.animations.failVideo; + if (videoData) { + H5P.playVideo($myDom, videoData, params.endGame.animations.skipButtonText, cp, function () { + displayResults(); + }); + return; + } + } + // Trigger finished event. + displayResults(); + }; + + // Function for attaching the multichoice to a DOM element. + var attach = function (target) { + if (typeof(target) == "string") { + $myDom = $("#" + target); + } else { + $myDom = $(target); + } + + // Render own DOM into target. + $myDom.html(template.render(params)).css({ + backgroundImage: 'url(' + cp + params.backgroundImage.path + ')' + }); + + // Attach questions + for (var i=0; i= 0; i--) { + score += questionInstances[i].getScore(); + } + return score; + }; + + // Get total score possible for questionset. + var totalScore = function () { + var score = 0; + for (var i = questionInstances.length - 1; i >= 0; i--) { + score += questionInstances[i].totalScore(); + } + return score; + }; + + // Masquerade the main object to hide inner properties and functions. + var returnObject = { + attach: attach, // Attach to DOM object + getQuestions: function () {return questionInstances;}, + getScore: getScore, + totalScore: totalScore, + defaults: defaults // Provide defaults for inspection + }; + return returnObject; +}; diff --git a/library.json b/library.json new file mode 100644 index 0000000..cd31dac --- /dev/null +++ b/library.json @@ -0,0 +1,24 @@ +{ + "title": "Question set", + "contentType": "question", + "majorVersion": 1, + "minorVersion": 0, + "patchVersion": 6, + "runnable": 1, + "machineName": "H5P.QuestionSet", + "author": "Amendor AS", + "license": "cc-by-sa", + "preloadedJs": [ + {"path": "js/questionset.js"} + ], + "preloadedCss": [ + {"path": "css/questionset.css"} + ], + "preloadedDependencies": [ + { + "machineName": "EmbeddedJS", + "majorVersion": 1, + "minorVersion": 0 + } + ] +} diff --git a/semantics.json b/semantics.json new file mode 100644 index 0000000..d1ef7e8 --- /dev/null +++ b/semantics.json @@ -0,0 +1,221 @@ +[ + { + "name": "title", + "type": "text", + "label": "Title", + "description": "Question set title (optional)." + }, + { + "name": "randomOrder", + "type": "boolean", + "label": "Randomize order", + "description": "Whether questions should be shown in random order." + }, + { + "name": "initialQuestion", + "type": "number", + "label": "Initial question", + "description": "Which question to start with. Count from 0." + }, + { + "name": "backgroundImage", + "type": "image", + "label": "Background image", + "description": "An optional background image for the Question set." + }, + { + "name": "progressType", + "type": "select", + "label": "Progress indicator", + "description": "Question set progress indicator style.", + "values": [ + { + "text": "Textual", + "value": "textual" + }, + { + "text": "Dots", + "value": "dots" + } + ], + "default": "textual" + }, + { + "name": "passPercentage", + "type": "number", + "label": "Pass percentage", + "description": "Percentage of total score required for passing the quiz.", + "min": 0, + "max": 100, + "step": 1, + "default": 50 + }, + { + "name": "questions", + "type": "list", + "entity": "question", + "field": { + "name": "question", + "type": "group", + "label": "Question", + "fields": [ + { + "name": "library", + "type": "library", + "label": "Question library", + "description": "Library for this question.", + "options": [ + "H5P.MultiChoice 1.0" + ] + } + ] + } + }, + { + "name": "introPage", + "type": "group", + "label": "Intro page", + "description": "Data for the intro page.", + "fields": [ + { + "name": "showIntroPage", + "type": "boolean", + "label": "Show intro page?" + }, + { + "name": "title", + "type": "text", + "label": "Title" + }, + { + "name": "introduction", + "type": "text", + "label": "Introduction text" + }, + { + "name": "startButtonText", + "type": "text", + "label": "Start button text" + } + ] + }, + { + "name": "texts", + "type": "group", + "label": "Interface texts in quiz", + "common": true, + "fields": [ + { + "name": "prevButton", + "type": "text", + "label": "Back button", + "default": "Previous" + }, + { + "name": "nextButton", + "type": "text", + "label": "Next button", + "default": "Next" + }, + { + "name": "finishButton", + "type": "text", + "label": "Finish button", + "default": "Finish" + }, + { + "name": "textualProgress", + "type": "text", + "label": "Progress text", + "description": "Text used if textual progress is selected.", + "default": "Question: @current of @total questions" + } + ] + }, + { + "name": "endGame", + "type": "group", + "label": "End game data", + "fields": [ + { + "name": "showResultPage", + "type": "boolean", + "label": "Show result page", + "default": true + }, + { + "name": "resultPage", + "type": "group", + "label": "Result page", + "description": "Data and texts for the result page.", + "fields": [ + { + "name": "successGreeting", + "type": "text", + "label": "Success greeting", + "description": "Title in result page on success." + }, + { + "name": "successComment", + "type": "text", + "label": "Success comment", + "description": "Comment shown after the score." + }, + { + "name": "failGreeting", + "type": "text", + "label": "Failed greeting", + "description": "Title in result page on failed quiz." + }, + { + "name": "failComment", + "type": "text", + "label": "Failed comment", + "description": "Comment shown after the score on failed quiz." + }, + { + "name": "finishButtonText", + "type": "text", + "label": "Finish button text", + "description": "Text for the finish button." + } + ] + }, + { + "name": "animations", + "type": "group", + "label": "Animations", + "fields": [ + { + "name": "showAnimations", + "type": "boolean", + "label": "Show animations" + }, + { + "name": "skippable", + "type": "boolean", + "label": "Skippable" + }, + { + "name": "skipButtonText", + "type": "text", + "label": "Skip button text", + "common": true + }, + { + "name": "successVideo", + "type": "video", + "label": "Success video", + "description": "Video displayed on successful quiz." + }, + { + "name": "failVideo", + "type": "video", + "label": "Fail video", + "description": "Video displayed on failed quiz." + } + ] + } + ] + } +] diff --git a/views/questionset-results.ejs b/views/questionset-results.ejs new file mode 100644 index 0000000..722776d --- /dev/null +++ b/views/questionset-results.ejs @@ -0,0 +1,6 @@ +
+
<%= greeting %>
+
<%= score %>
+
<%= resulttext %>
+ +
diff --git a/views/questionset.ejs b/views/questionset.ejs new file mode 100644 index 0000000..5219d42 --- /dev/null +++ b/views/questionset.ejs @@ -0,0 +1,31 @@ +<% if (introPage.showIntroPage) { %> +
+
<%= introPage.title %>
+
<%= introPage.introduction %>
+ +
+<%} %> +