diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..86c445f
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015", "react"]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..019c587
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+build
+dist
+node_modules
+worknotes
diff --git a/.h5pignore b/.h5pignore
new file mode 100644
index 0000000..eaa2e7e
--- /dev/null
+++ b/.h5pignore
@@ -0,0 +1,18 @@
+build
+node_modules
+reports
+tests
+dev
+src
+.idea
+.git
+.babelrc
+.gitmodules
+.h5pignore
+.travis.yml
+karma.conf.js
+webpack.config.js
+README.md
+CONTRIBUTING.md
+package.json
+worknotes
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a414cc0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+language: node_js
+node_js:
+ - "stable"
+
+cache:
+ directories:
+ - node_modules
+
+# Google Chrome
+#
+# https://github.com/travis-ci/travis-ci/issues/272#issuecomment-14402117
+# http://stackoverflow.com/questions/19255976/how-to-make-travis-execute-angular-tests-on-chrome-please-set-env-variable-chr
+#
+before_install:
+ - export CHROME_BIN=chromium-browser
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..f16d333
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,4 @@
+Please check out existing repo issues, and the issues in this document.
+Create new issues in the repository if it doesn't exist or collaborate with
+others if it already exists.
+Create a pull request with test coverage for the issue :)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f447cdc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# h5p-survey
+
+[](https://travis-ci.org/h5p/h5p-survey)
+
+A H5P library for creating interactive surveys.
+
+## Getting started
+
+Grab all the modules and build the project:
+```javascript
+npm start
+```
+
+Run tests:
+```javscript
+npm test
+```
+
+Set up development server with test data:
+```javascript
+npm run dev
+```
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..569d653
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,86 @@
+// Karma configuration
+// Generated on Fri Aug 05 2016 15:01:10 GMT+0200 (Vest-Europa (sommertid))
+var path = require('path');
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'tests/**/*.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'tests/**/*.js': ['webpack']
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+ webpack: {
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ include: [
+ path.resolve(__dirname, "tests"),
+ path.resolve(__dirname, "src/scripts")
+ ],
+ loader: 'babel'
+ }
+ ]
+ },
+ },
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['Chrome', 'Firefox'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity
+ })
+};
diff --git a/library.json b/library.json
new file mode 100644
index 0000000..d307744
--- /dev/null
+++ b/library.json
@@ -0,0 +1,14 @@
+{
+ "title": "Simple Multi Choice",
+ "description": "Create a simple multiple choice",
+ "majorVersion": 1,
+ "minorVersion": 0,
+ "patchVersion": 0,
+ "runnable": 1,
+ "author": "thomasmars",
+ "license": "MIT",
+ "machineName": "H5P.SimpleMultiChoice",
+ "preloadedJs": [
+ {"path": "dist/dist.js"}
+ ]
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1b1d9a0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "simpleMultipleChoice",
+ "version": "1.0.0",
+ "description": "Create lightweight multiple choice for use inside content types",
+ "scripts": {
+ "test": "karma start",
+ "testdev": "karma start --autoWatch --no-single-run",
+ "build": "webpack --progress -p",
+ "dev": "webpack -d && webpack-dev-server -d --inline --hot --open"
+ },
+ "keywords": [
+ "lightweight",
+ "multiple choice",
+ "h5p",
+ "library"
+ ],
+ "author": "thomasmars",
+ "license": "MIT",
+ "devDependencies": {
+ "babel-cli": "^6.6.5",
+ "babel-loader": "^6.2.4",
+ "babel-preset-es2015": "^6.6.0",
+ "babel-preset-react": "^6.11.1",
+ "coveralls": "^2.11.9",
+ "css-loader": "^0.23.1",
+ "exports-loader": "^0.6.3",
+ "expose-loader": "^0.7.1",
+ "file-loader": "^0.9.0",
+ "h5p-view": "^1.0.0",
+ "imports-loader": "^0.6.5",
+ "jasmine-core": "^2.4.1",
+ "json-loader": "^0.5.4",
+ "karma": "^1.1.2",
+ "karma-chrome-launcher": "^1.0.1",
+ "karma-firefox-launcher": "^1.0.0",
+ "karma-jasmine": "^1.0.2",
+ "karma-webpack": "^1.7.0",
+ "react": "^15.3.0",
+ "react-dom": "^15.3.0",
+ "style-loader": "^0.13.1",
+ "webpack": "^1.12.14"
+ }
+}
diff --git a/semantics.json b/semantics.json
new file mode 100644
index 0000000..70adf2d
--- /dev/null
+++ b/semantics.json
@@ -0,0 +1,34 @@
+[
+ {
+ "name": "question",
+ "label": "Question",
+ "type": "text"
+ },
+ {
+ "name": "inputType",
+ "label": "Multiple choice type",
+ "type": "select",
+ "options": [
+ {
+ "label": "Checkbox",
+ "value": "checkbox"
+ },
+ {
+ "label": "Radiobox",
+ "value": "radio"
+ }
+ ]
+ },
+ {
+ "name": "alternatives",
+ "label": "Answer alternatives",
+ "type": "list",
+ "entity": "Alternative",
+ "min": 2,
+ "field": {
+ "name": "alternative",
+ "label": "Alternative",
+ "type": "text"
+ }
+ }
+]
diff --git a/src/content/devCheck.json b/src/content/devCheck.json
new file mode 100644
index 0000000..db79c6e
--- /dev/null
+++ b/src/content/devCheck.json
@@ -0,0 +1,9 @@
+{
+ "question": "What do you like the most ?",
+ "inputType": "checkbox",
+ "alternatives": [
+ "Fish",
+ "Turtles",
+ "Icecream"
+ ]
+}
diff --git a/src/content/devRadio.json b/src/content/devRadio.json
new file mode 100644
index 0000000..4bb4d2e
--- /dev/null
+++ b/src/content/devRadio.json
@@ -0,0 +1,9 @@
+{
+ "question": "What do you like the most ?",
+ "inputType": "radio",
+ "alternatives": [
+ "Fish",
+ "Turtles",
+ "Icecream"
+ ]
+}
diff --git a/src/entries/dev.js b/src/entries/dev.js
new file mode 100644
index 0000000..b86b297
--- /dev/null
+++ b/src/entries/dev.js
@@ -0,0 +1,8 @@
+import 'expose?H5P!exports?H5P!h5p-view';
+import SimpleMultiChoice from '../scripts/simple-multiple-choice';
+
+var paramsCheck = require('../content/devCheck.json');
+var paramsRadio = require('../content/devRadio.json');
+
+new SimpleMultiChoice(paramsCheck).attach(H5P.jQuery('
').appendTo(H5P.jQuery('body')));
+new SimpleMultiChoice(paramsRadio).attach(H5P.jQuery('
').appendTo(H5P.jQuery('body')));
diff --git a/src/entries/dist.js b/src/entries/dist.js
new file mode 100644
index 0000000..2e1f2ad
--- /dev/null
+++ b/src/entries/dist.js
@@ -0,0 +1,2 @@
+// Load library
+H5P.SimpleMultiChoice = require('../scripts/simple-multiple-choice').default;
diff --git a/src/scripts/simple-multiple-choice.js b/src/scripts/simple-multiple-choice.js
new file mode 100644
index 0000000..175bcef
--- /dev/null
+++ b/src/scripts/simple-multiple-choice.js
@@ -0,0 +1,108 @@
+let instanceId = 0;
+
+export default class SimpleMultiChoice extends H5P.EventDispatcher {
+
+ /**
+ * Constructor for survey
+ * @param {Object} params
+ * @param {string} params.question Question text
+ * @param {string} params.inputType Checkbox or radio
+ * @param {Array} params.alternatives Array of strings with answers alternatives
+ * @param {number} contentId
+ */
+ constructor(params, contentId = null) {
+ super();
+ this.params = params;
+
+ // Provide a unique identifier for each multi choice
+ this.uniqueName = 'h5p-simple-multiple-choice-' + instanceId;
+ instanceId += 1;
+
+ // Keep track of the state
+ this.state = this.params.alternatives.map((alt, i) => {
+ return {
+ id: i,
+ text: alt,
+ checked: false
+ }
+ });
+ }
+
+ /**
+ * Attach library to wrapper
+ * @param {jQuery} $wrapper
+ */
+ attach($wrapper) {
+ const element = document.createElement('div');
+ element.className = 'h5p-simple-multiple-choice';
+ const question = this.createQuestion();
+ question.className = 'h5p-simple-multiple-choice-question';
+ element.appendChild(question);
+
+ const altList = this.createAlternativesList(this.params.alternatives);
+ element.appendChild(altList);
+
+ $wrapper.get(0).appendChild(element);
+ }
+
+ /**
+ * Create html for multiple choice
+ * @return {HTMLElement} html for multiple choice
+ */
+ createQuestion() {
+ const question = document.createElement('div');
+ question.textContent = this.params.question;
+ return question;
+ }
+
+ /**
+ * Handle input changed, trigger event for listeners
+ * @param {number} inputIndex Index of input element that changed
+ */
+ handleInputChange(inputIndex) {
+ this.state = this.state.map((alt, j) => {
+ let checked = j === inputIndex;
+ if (this.params.inputType !== 'radio') {
+ checked = j === inputIndex ? !alt.checked : alt.checked;
+ }
+
+ // Immutable state
+ return Object.assign({}, alt, {
+ checked: checked
+ });
+ });
+
+ this.trigger('changed', this.state);
+ }
+
+ /**
+ * Create alternatives for multiple choice
+ * @param {Array.} alternatives Answer alternatives
+ * @return {HTMLElement} html for alternatives list items
+ */
+ createAlternativesList(alternatives) {
+ const altList = document.createElement('ul');
+ altList.className = 'h5p-simple-multiple-choice-alternatives';
+ alternatives.forEach((alt, i) => {
+
+ // Elements
+ const listItem = document.createElement('li');
+ const label = document.createElement('label');
+ const input = document.createElement('input');
+
+ // Input attributes
+ input.type = this.params.inputType;
+ input.name = this.uniqueName;
+
+ // Label attributes
+ label.addEventListener('change', this.handleInputChange.bind(this, i));
+ label.appendChild(input);
+ label.innerHTML += alt;
+
+ listItem.appendChild(label);
+ altList.appendChild(listItem);
+ });
+
+ return altList;
+ }
+}
diff --git a/tests/simple-multiple-choice.js b/tests/simple-multiple-choice.js
new file mode 100644
index 0000000..8595ffe
--- /dev/null
+++ b/tests/simple-multiple-choice.js
@@ -0,0 +1,194 @@
+import 'expose?H5P!exports?H5P!h5p-view';
+import SimpleMultipleChoice from '../src/scripts/simple-multiple-choice';
+
+describe('Simple Multiple Choice', () => {
+ const params = {
+ question: 'What do you like the most ?',
+ inputType: 'checkbox',
+ alternatives: [
+ 'Fish',
+ 'Turtles',
+ 'Icecream'
+ ]
+ };
+
+ const $body = H5P.jQuery('body');
+
+ beforeEach(() => {
+ spyOn(H5P, 'newRunnable');
+ });
+
+ describe('General', () => {
+ const simpleMultiChoice = new SimpleMultipleChoice(params);
+ simpleMultiChoice.attach($body);
+ const surveyElement = $body.get(0).querySelectorAll('.h5p-simple-multiple-choice');
+
+
+ // Check that attach is called
+ it('should attach to the $wrapper', () => {
+ expect(surveyElement[0].parentNode).toBe($body.get(0));
+ });
+
+ // Check that all survey elements are called with newRunnable
+ it('should display question', () => {
+ const question = document.querySelector('.h5p-simple-multiple-choice-question');
+ expect(question.textContent).toBe(params.question);
+ });
+
+ it('should create 3 alternatives', () => {
+ const alternatives = document.querySelector('.h5p-simple-multiple-choice-alternatives');
+ expect(alternatives.children.length).toEqual(3);
+ });
+
+ it('should trigger immutable states', (done) => {
+ const alternatives = document.querySelector('.h5p-simple-multiple-choice-alternatives');
+ const inputs = alternatives.querySelectorAll('input');
+ let changed = 0;
+ let prevState = null;
+
+ simpleMultiChoice.on('changed', (state) => {
+ if (changed > 0) {
+ simpleMultiChoice.off('changed');
+ }
+ else {
+ prevState = state;
+ }
+
+ //Wait for DOM
+ setTimeout(() => {
+ if (changed > 0) {
+ console.log("prevState", prevState.data);
+ console.log("state", state.data);
+ expect(state.data == prevState.data).toBeFalsy();
+ setTimeout(() => {
+ done();
+ }, 100);
+ }
+ else {
+ changed += 1;
+ inputs[2].click();
+ }
+ }, 100);
+ });
+ inputs[1].click();
+ });
+ });
+
+ describe('Checkboxes', () => {
+ const simpleCheckboxMultiChoice = new SimpleMultipleChoice(params);
+ simpleCheckboxMultiChoice.attach($body);
+ const alternatives = document.querySelectorAll('.h5p-simple-multiple-choice-alternatives')[1];
+ const inputs = alternatives.querySelectorAll('input');
+
+ it ('input field should be a checkbox', () => {
+ expect(inputs[0].type).toBe('checkbox');
+ });
+
+ it('should trigger state change when checked', (done) => {
+ simpleCheckboxMultiChoice.on('changed', (state) => {
+ simpleCheckboxMultiChoice.off('changed');
+
+ //Wait for DOM
+ setTimeout(() => {
+ expect(state).toBeDefined();
+ done();
+ }, 100);
+ });
+ inputs[0].click();
+ });
+
+ it('should check when checked', (done) => {
+ simpleCheckboxMultiChoice.on('changed', (state) => {
+ simpleCheckboxMultiChoice.off('changed');
+
+ //Wait for DOM
+ setTimeout(() => {
+ expect(state.data[0].checked).toBeTruthy();
+ expect(state.data[1].checked).toBeTruthy();
+ expect(state.data[2].checked).toBeFalsy();
+ done();
+ }, 100);
+ });
+ inputs[1].click();
+ });
+
+ it('should uncheck when unchecked', (done) => {
+ simpleCheckboxMultiChoice.on('changed', (state) => {
+ simpleCheckboxMultiChoice.off('changed');
+
+ //Wait for DOM
+ setTimeout(() => {
+ expect(state.data[0].checked).toBeTruthy();
+ expect(state.data[1].checked).toBeFalsy();
+ expect(state.data[2].checked).toBeFalsy();
+ done();
+ }, 100);
+ });
+ inputs[1].click();
+ });
+ });
+
+ describe('Radiobuttons', () => {
+
+ const radioParams = Object.assign({}, params, {
+ inputType: 'radio'
+ });
+
+ const simpleRadiobuttonMultiChoice = new SimpleMultipleChoice(radioParams);
+ simpleRadiobuttonMultiChoice.attach($body);
+
+ const alternatives = document.querySelectorAll('.h5p-simple-multiple-choice-alternatives');
+ const inputs = alternatives[2].querySelectorAll('input');
+
+ it('should check when checked', (done) => {
+ simpleRadiobuttonMultiChoice.on('changed', (state) => {
+ simpleRadiobuttonMultiChoice.off('changed');
+
+ //Wait for DOM
+ setTimeout(() => {
+ expect(state.data[0].checked).toBeTruthy();
+ done();
+ }, 100);
+ });
+ inputs[0].click();
+ });
+
+ it('should not uncheck when checked again', () => {
+ inputs[0].click();
+ // Will not fire 'changed' event.
+ expect(inputs[0].checked).toBeTruthy();
+ });
+
+ it('should uncheck old and check new when checking a new checkbox', (done) => {
+ simpleRadiobuttonMultiChoice.on('changed', (state) => {
+ simpleRadiobuttonMultiChoice.off('changed');
+
+ //Wait for DOM
+ setTimeout(() => {
+ expect(state.data[0].checked).toBeFalsy();
+ expect(state.data[1].checked).toBeTruthy();
+ expect(state.data[2].checked).toBeFalsy();
+ done();
+ }, 100);
+ });
+ inputs[1].click();
+ })
+ });
+
+ describe('Multiple instances', () => {
+ const alternatives = document.querySelectorAll('.h5p-simple-multiple-choice-alternatives');
+ const checkboxInputs = alternatives[1].querySelectorAll('input');
+ const radioInputs = alternatives[2].querySelectorAll('input');
+
+ it('should have different names', () => {
+ expect(checkboxInputs[0].name).toBeDefined();
+ expect(radioInputs[0].name).toBeDefined();
+ expect(checkboxInputs[0].name).not.toBe(radioInputs[0].name);
+ });
+
+ it('same instance inputs should have same name', () => {
+ expect(checkboxInputs[0].name).toBe(checkboxInputs[1].name);
+ expect(radioInputs[0].name).toBe(radioInputs[1].name);
+ })
+ })
+});
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..8eb32b2
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,49 @@
+var webpack = require('webpack');
+var path = require('path');
+
+module.exports = {
+ entry: {
+ '../dist/dist': "./src/entries/dist.js",
+ 'dev': "./src/entries/dev.js"
+ },
+ output: {
+ path: path.join(__dirname, '/build'),
+ filename: "[name].js",
+ sourceMapFileName: "[file].map"
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ include: [
+ path.resolve(__dirname, "src/scripts"),
+ path.resolve(__dirname, "src/entries")
+ ],
+ loader: 'babel'
+ },
+ {
+ test: /\.css$/,
+ include: path.resolve(__dirname, "src/scripts"),
+ loader: "style!css?sourceMap&modules"
+ },
+ {
+ test: /\.json$/,
+ include: path.resolve(__dirname, "src/content"),
+ loader: 'json'
+ }
+ ]
+ },
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env': {
+ 'NODE_ENV': JSON.stringify('production')
+ }
+ })
+ ],
+ devtool: 'source-map',
+ devServer: {
+ port: 8050,
+ contentBase: "./build",
+ quiet: true,
+ }
+};