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 %>
' +
+'
' +
+'
' +
+'<%} %>' +
+'' +
+'
<%= title %>
' +
+' <% for (var i=0; i
' +
+' ' +
+'
<%= questions[i].library %>
' +
+'
' +
+' <% } %>' +
+' ' +
+' ' +
+ '';
+ var resulttemplate = '' +
+'' +
+'
<%= greeting %>
' +
+'
<%= score %>
' +
+'
<%= resulttext %>
' +
+'
' +
+'
' +
+ '';
+
+ 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 %>
+
+
+<%} %>
+
+
<%= title %>
+ <% for (var i=0; i
+
+
<%= questions[i].name %>
+
+ <% } %>
+
+