From: Aleksander Ɓukasz Date: Mon, 7 Jul 2014 07:44:28 +0000 (+0200) Subject: editor: core plugin - edumed - first approach to all three choice exercises X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/6d7963d9e1a726e9e8d2173b7df165c63421f546?hp=3dfe15d50b60598341b9754876db4a48162c5cd0 editor: core plugin - edumed - first approach to all three choice exercises --- diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index 3dac88f..ebede40 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -97,6 +97,15 @@ var Canvas = function(wlxmlDocument, elements, metadata, sandbox) { $.extend(Canvas.prototype, Backbone.Events, { + createElementType: function(elementPrototype) { + /* TODO: reconcile this with ElementsRegister behavior */ + var Constructor = function() { + documentElement.DocumentNodeElement.apply(this, Array.prototype.slice.call(arguments, 0)); + }; + Constructor.prototype = elementPrototype; + return Constructor; + }, + getElementOffset: function(element) { return element.dom.offset().top - this.dom.offset().top; }, diff --git a/src/editor/plugins/core/edumed/choice/choice.less b/src/editor/plugins/core/edumed/choice/choice.less new file mode 100644 index 0000000..fb99110 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choice.less @@ -0,0 +1 @@ +@import 'choiceTrueOrFalse.less'; \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/choiceBase.js b/src/editor/plugins/core/edumed/choice/choiceBase.js new file mode 100644 index 0000000..dc8fe0a --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceBase.js @@ -0,0 +1,61 @@ +define(function(require) { + +'use strict'; + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + documentElement = require('modules/documentCanvas/canvas/documentElement'), + template = require('libs/text!./element.html'); + + +var choiceBase = Object.create(documentElement.DocumentNodeElement.prototype); +_.extend(choiceBase, { + init: function() { + documentElement.DocumentNodeElement.prototype.init.call(this); + this.view = $(_.template(template)({type: this.type})); + this._container().append(this.view); + + this.createContainer(this.wlxmlNode.contents().filter(function(n) {return !n.is('list');}), { + manages: function(node, removedFrom) { + if(node.is('list.orderable')) { + return false; + } + return this.wlxmlNode.sameNode(node.parent() || removedFrom); + }.bind(this), + dom: this.view.find('.description') + }); + + this.wlxmlNode.contents() + .filter(function(node) { return node.is('list'); }) + .some(function(node) { + this.listView = this.createListView(node); + this.view.find('.list').append(this.listView.dom); + }.bind(this)); + }, + getVerticallyFirstTextElement: function() { + var toret; + this.containers.some(function(container) { + toret = container.getVerticallyFirstTextElement(); + return !!toret; + }); + return toret; + }, + onNodeAdded: function(event) { + var node = event.meta.node; + if(this.listView.listNode.sameNode(node.parent()) && node.is('item.answer')) { + this.listView.addItem(node); + } + }, + onNodeDetached: function(event) { + var node = event.meta.node; + if(this.listView.listNode.sameNode(event.meta.parent) && node.is('item.answer')) { + this.listView.removeItem(node); + } + } +}); + +return choiceBase; + +}); + + diff --git a/src/editor/plugins/core/edumed/choice/choiceMulti.js b/src/editor/plugins/core/edumed/choice/choiceMulti.js new file mode 100644 index 0000000..a05346c --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceMulti.js @@ -0,0 +1,49 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + choiceBase = require('./choiceBase'), + ListView = require('./list'); + + +var choiceMulti = Object.create(choiceBase); +_.extend(choiceMulti, { + type: 'multi', + createListView: function(listNode) { + return new ListView(this, listNode, { + onItemViewAdded: function(itemView) { + var checkbox = new CheckboxView(itemView.node.getAttr('answer') === 'true', function(checked) { + itemView.node.document.transaction(function() { + itemView.node.getParent('exercise.choice').object.setAnswer(itemView.node, checked); + }, { + metadata: { + description: gettext('Change answer') + } + }); + }); + itemView.addPrefixView(checkbox); + } + }); + }, + onNodeAttrChange: function(event) { + if(this.listView.listNode.sameNode(event.meta.node.parent())) { + this.listView.getItemView(event.meta.node).prefixView.dom.attr('checked', event.meta.newVal === 'true'); + } + } +}); + +var CheckboxView = function(checked, onValueChange) { + this.dom = $('') + .attr('checked', checked); + this.dom.on('click', function() { + onValueChange(this.checked); + }); +}; + +return choiceMulti; + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/choiceSingle.js b/src/editor/plugins/core/edumed/choice/choiceSingle.js new file mode 100644 index 0000000..9346363 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceSingle.js @@ -0,0 +1,56 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + choiceBase = require('./choiceBase'), + ListView = require('./list'); + + +var choiceSingle = Object.create(choiceBase); +_.extend(choiceSingle, { + type: 'single', + init: function() { + this._comboName = _.uniqueId('edumed_exercise_hash_'); + choiceBase.init.call(this); + }, + createListView: function(listNode) { + var el = this; + return new ListView(this, listNode, { + onItemViewAdded: function(itemView) { + var radiobox = new RadioView(itemView.node.getAttr('answer') === 'true', el._comboName, function() { + itemView.node.document.transaction(function() { + itemView.node.getParent('exercise.choice').object.markAsAnswer(itemView.node); + }, { + metadata: { + description: gettext('Change answer') + } + }); + + }); + itemView.addPrefixView(radiobox); + } + }); + }, + onNodeAttrChange: function(event) { + if(this.listView.listNode.sameNode(event.meta.node.parent())) { + this.listView.getItemView(event.meta.node).prefixView.dom.attr('checked', event.meta.newVal === 'true'); + } + } +}); + +var RadioView = function(checked, name, onValueChange) { + this.dom = $('') + .attr('checked', checked) + .attr('name', name); + this.dom.on('change', function() { + onValueChange(this.checked); + }); +}; + +return choiceSingle; + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.js b/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.js new file mode 100644 index 0000000..7ac0470 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.js @@ -0,0 +1,56 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + choiceBase = require('./choiceBase'), + switchTemplate = require('libs/text!./trueOrFalseSwitch.html'), + ListView = require('./list'); + + +var trueOrFalse = Object.create(choiceBase); +_.extend(trueOrFalse, { + type: 'true-or-false', + createListView: function(listNode) { + return new ListView(this, listNode, { + onItemViewAdded: function(itemView) { + var switchView = new Switch(itemView.node.getAttr('answer') === 'true', function(choice) { + itemView.node.document.transaction(function() { + itemView.node.getParent('exercise.choice').object.setAnswer(itemView.node, choice); + }, { + metadata: { + description: gettext('Change answer') + } + }); + }); + itemView.addPrefixView(switchView); + } + }); + } +}); + +var Switch = function(checked, onValueChange) { + this.dom = $(_.template(switchTemplate)()); + var trueBtn = this.dom.find('.true'), + falseBtn = this.dom.find('.false'); + + trueBtn.on('click', function() { + trueBtn.addClass('selected'); + falseBtn.removeClass('selected'); + onValueChange(true); + }); + this.dom.find('.false').on('click', function() { + falseBtn.addClass('selected'); + trueBtn.removeClass('selected'); + onValueChange(false); + }); + trueBtn.toggleClass('selected', checked); + falseBtn.toggleClass('selected', !checked); +}; + +return trueOrFalse; + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.less b/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.less new file mode 100644 index 0000000..40d0318 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.less @@ -0,0 +1,19 @@ +.exercise-true-or-false { + ul { + list-style: none; + } + .switch { + font-variant: small-caps; + float: left; + margin-right: 15px; + background-color: #d4d6d8; + cursor: pointer; + border-radius: 5px; + padding: 0px 5px; + font-weight: bold; + .selected { + color: white; + background-color: #ed7831; + } + } +} \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/element.html b/src/editor/plugins/core/edumed/choice/element.html new file mode 100644 index 0000000..ba4bdd0 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/element.html @@ -0,0 +1,5 @@ +
+
<%= gettext('Exercise') %>: <%= type %>
+
+
+
\ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/element.js b/src/editor/plugins/core/edumed/choice/element.js new file mode 100644 index 0000000..0514dae --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/element.js @@ -0,0 +1,15 @@ +define(function(require) { + +'use strict'; + +var choiceSingle = require('./choiceSingle'), + choiceMulti = require('./choiceMulti'), + choiceTrueOrFalse = require('./choiceTrueOrFalse'); + +return [ + {tag: 'div', klass: 'exercise.choice', prototype: choiceMulti}, + {tag: 'div', klass: 'exercise.choice.single', prototype: choiceSingle}, + {tag: 'div', klass: 'exercise.choice.true-or-false', prototype: choiceTrueOrFalse}, +]; + +}); diff --git a/src/editor/plugins/core/edumed/choice/list.html b/src/editor/plugins/core/edumed/choice/list.html new file mode 100644 index 0000000..11c4e94 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/list.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/editor/plugins/core/edumed/choice/list.js b/src/editor/plugins/core/edumed/choice/list.js new file mode 100644 index 0000000..e3fbd84 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/list.js @@ -0,0 +1,97 @@ +define(function(require) { + +'use strict'; +var $ = require('libs/jquery'); + +var _ = require('libs/underscore'), + Backbone = require('libs/backbone'), + viewTemplate = require('libs/text!./list.html'), + viewItemTemplate = require('libs/text!./listItem.html'); + + +var ListView = function(element, listNode, params) { + this.element = element; + this.listNode = listNode; + this.params = params; + this.dom = $(_.template(viewTemplate)()); + this.list = this.dom.find('ul'); + this.addButton = this.dom.find('button.add'); + this.itemViews = []; + + this.listNode.contents() + .filter(function(node) { + return node.is('item.answer'); + }) + .forEach(function(node) { + this.addItem(node); + }.bind(this)); +}; +_.extend(ListView.prototype, Backbone.Events, { + addItem: function(node) { + var view = new ItemView(node, this); + var idx = this.listNode.contents() + .filter(function(n) { return n.is('item'); }) + .indexOf(node); + + if(idx <= this.itemViews.length - 1) { + this.itemViews.splice(idx, 0, view); + } else { + this.itemViews.push(view); + } + if(idx > 0) { + this.itemViews[idx-1].dom.after(view.dom); + } else { + this.list.prepend(view.dom); + } + if(this.params.onItemViewAdded) { + this.params.onItemViewAdded(view); + } + }, + removeItem: function(node) { + this.itemViews.some(function(view, idx) { + if(view.node.sameNode(node)) { + view.remove(); + this.itemViews.splice(idx, 1); + return true; + } + }.bind(this)); + }, + getItemView: function(node) { + var toret; + this.itemViews.some(function(view) { + if(view.node.sameNode(node)) { + toret = view; + return true; + } + }.bind(this)); + return toret; + } +}); + +var ItemView = function(node, exerciseView) { + this.node = node; + this.exerciseView = exerciseView; + this.dom = $(_.template(viewItemTemplate)()); + + this.container = exerciseView.element.createContainer(node.contents(), { + manages: function(node, originaParent) { + return this.node.sameNode(node.parent() || originaParent); + }.bind(this), + dom: this.dom.find('.content') + }); +}; + +_.extend(ItemView.prototype, Backbone.Events, { + remove: function() { + this.container.remove(); + this.dom.remove(); + }, + addPrefixView: function(view) { + this.dom.find('.prefix').append(view.dom); + this.prefixView = view; + } +}); + +return ListView; + +}); diff --git a/src/editor/plugins/core/edumed/choice/listItem.html b/src/editor/plugins/core/edumed/choice/listItem.html new file mode 100644 index 0000000..4735c9b --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/listItem.html @@ -0,0 +1 @@ +
  • diff --git a/src/editor/plugins/core/edumed/choice/trueOrFalseSwitch.html b/src/editor/plugins/core/edumed/choice/trueOrFalseSwitch.html new file mode 100644 index 0000000..ee89828 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/trueOrFalseSwitch.html @@ -0,0 +1,3 @@ +
    + <%= gettext('true') %><%= gettext('false') %> +
    \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/edumed.js b/src/editor/plugins/core/edumed/edumed.js index e4a330b..6ba5996 100644 --- a/src/editor/plugins/core/edumed/edumed.js +++ b/src/editor/plugins/core/edumed/edumed.js @@ -7,11 +7,12 @@ var actions = require('./actions'), replaceActions = require('./replace/actions'), orderExerciseElement = require('./order/element'), gapsExerciseElement = require('./gaps/element'), - replaceExerciseElement = require('./replace/element'); + replaceExerciseElement = require('./replace/element'), + choiceExerciseElements = require('./choice/element'); return { actions: actions.concat(gapsActions).concat(replaceActions), - canvasElements: [orderExerciseElement, gapsExerciseElement, replaceExerciseElement] + canvasElements: [orderExerciseElement, gapsExerciseElement, replaceExerciseElement].concat(choiceExerciseElements) }; }); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/edumed.less b/src/editor/plugins/core/edumed/edumed.less index e1a2c4d..b503d5a 100644 --- a/src/editor/plugins/core/edumed/edumed.less +++ b/src/editor/plugins/core/edumed/edumed.less @@ -1,6 +1,7 @@ @import 'order/order.less'; @import 'gaps/gaps.less'; @import 'replace/replace.less'; +@import 'choice/choice.less'; .edumed-exercise { diff --git a/src/wlxml/extensions/edumed/edumed.js b/src/wlxml/extensions/edumed/edumed.js index 3176ae0..fb0885a 100644 --- a/src/wlxml/extensions/edumed/edumed.js +++ b/src/wlxml/extensions/edumed/edumed.js @@ -149,6 +149,59 @@ var extension = {wlxmlClass: {'exercise.order': { } }}}; +var choiceMethods = { + getChoiceList: function() { + return this.contents() + .filter(function(n) { return this.object.isChoiceList(n); }.bind(this))[0]; + }, + isChoiceList: function(node) { + return node.is('list') && this.sameNode(node.parent()); + }, + isChoiceListItem: function(node) { + return this.object.isChoiceList(node.parent()) && node.is('item.answer'); + } +}; + +extension.wlxmlClass['exercise.choice'] = { + transformations: { + setAnswer: function(itemNode, answer) { + if(!this.object.isChoiceListItem(itemNode)) { + return; + } + itemNode.setAttr('answer', answer ? 'true' : 'false'); + } + }, + methods: choiceMethods +}; + +extension.wlxmlClass['exercise.choice.single'] = { + transformations: { + markAsAnswer: function(itemNode) { + if(!this.object.isChoiceListItem(itemNode)) { + return; + } + this.object.getChoiceList().contents() + .filter(function(node) { return node.is('item.answer'); }) + .forEach(function(node) { + node.setAttr('answer', node.sameNode(itemNode) ? 'true' : 'false'); + }); + } + }, + methods: choiceMethods +}; + +extension.wlxmlClass['exercise.choice.true-or-false'] = { + transformations: { + setAnswer: function(itemNode, answer) { + if(!this.object.isChoiceListItem(itemNode)) { + return; + } + itemNode.setAttr('answer', answer ? 'true' : 'false'); + } + }, + methods: choiceMethods +}; + extension.document = { methods: { edumedCreateExerciseNode: function(klass) {