editor: core plugin - edumed - first approach to all three choice exercises
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Mon, 7 Jul 2014 07:44:28 +0000 (09:44 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Thu, 14 Aug 2014 14:26:13 +0000 (16:26 +0200)
16 files changed:
src/editor/modules/documentCanvas/canvas/canvas.js
src/editor/plugins/core/edumed/choice/choice.less [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/choiceBase.js [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/choiceMulti.js [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/choiceSingle.js [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.js [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.less [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/element.html [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/element.js [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/list.html [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/list.js [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/listItem.html [new file with mode: 0644]
src/editor/plugins/core/edumed/choice/trueOrFalseSwitch.html [new file with mode: 0644]
src/editor/plugins/core/edumed/edumed.js
src/editor/plugins/core/edumed/edumed.less
src/wlxml/extensions/edumed/edumed.js

index 3dac88f..ebede40 100644 (file)
@@ -97,6 +97,15 @@ var Canvas = function(wlxmlDocument, elements, metadata, sandbox) {
 
 $.extend(Canvas.prototype, Backbone.Events, {
 
 
 $.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;
     },
     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 (file)
index 0000000..fb99110
--- /dev/null
@@ -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 (file)
index 0000000..dc8fe0a
--- /dev/null
@@ -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 (file)
index 0000000..a05346c
--- /dev/null
@@ -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 = $('<input type="checkbox">')
+            .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 (file)
index 0000000..9346363
--- /dev/null
@@ -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 = $('<input type="radio" style="float: left; margin-right: 40px;">')
+            .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 (file)
index 0000000..7ac0470
--- /dev/null
@@ -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 (file)
index 0000000..40d0318
--- /dev/null
@@ -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 (file)
index 0000000..ba4bdd0
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="edumed-exercise exercise-<%= type %>">
+    <div class="header"><%= gettext('Exercise') %>: <%= type %></div>
+    <div class="description"></div>
+    <div class="list"></div>
+</div>
\ 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 (file)
index 0000000..0514dae
--- /dev/null
@@ -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 (file)
index 0000000..11c4e94
--- /dev/null
@@ -0,0 +1,3 @@
+<div class="exercise-list">
+    <ul></ul>
+</div>
\ 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 (file)
index 0000000..e3fbd84
--- /dev/null
@@ -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 (file)
index 0000000..4735c9b
--- /dev/null
@@ -0,0 +1 @@
+<li><span class="prefix"></span><span class="content"></span></li>
diff --git a/src/editor/plugins/core/edumed/choice/trueOrFalseSwitch.html b/src/editor/plugins/core/edumed/choice/trueOrFalseSwitch.html
new file mode 100644 (file)
index 0000000..ee89828
--- /dev/null
@@ -0,0 +1,3 @@
+<div class="switch">
+    <span class="true"><%= gettext('true') %></span><span class="false"><%= gettext('false') %></span>
+</div>
\ No newline at end of file
index e4a330b..6ba5996 100644 (file)
@@ -7,11 +7,12 @@ var actions = require('./actions'),
     replaceActions = require('./replace/actions'),
     orderExerciseElement = require('./order/element'),
     gapsExerciseElement = require('./gaps/element'),
     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),
 
 return {
     actions: actions.concat(gapsActions).concat(replaceActions),
-    canvasElements: [orderExerciseElement, gapsExerciseElement, replaceExerciseElement]
+    canvasElements: [orderExerciseElement, gapsExerciseElement, replaceExerciseElement].concat(choiceExerciseElements)
 };
 
 });
\ No newline at end of file
 };
 
 });
\ No newline at end of file
index e1a2c4d..b503d5a 100644 (file)
@@ -1,6 +1,7 @@
 @import 'order/order.less';
 @import 'gaps/gaps.less';
 @import 'replace/replace.less';
 @import 'order/order.less';
 @import 'gaps/gaps.less';
 @import 'replace/replace.less';
+@import 'choice/choice.less';
 
 .edumed-exercise {
 
 
 .edumed-exercise {
 
index 3176ae0..fb0885a 100644 (file)
@@ -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) {
 extension.document = {
     methods: {
          edumedCreateExerciseNode: function(klass) {