editor: core plugin - edumed - first take on order exercise
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Tue, 5 Aug 2014 09:14:46 +0000 (11:14 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Thu, 14 Aug 2014 14:26:13 +0000 (16:26 +0200)
src/editor/plugins/core/canvasElements.less
src/editor/plugins/core/core.js
src/editor/plugins/core/edumed/actions.js [new file with mode: 0644]
src/editor/plugins/core/edumed/edumed.js [new file with mode: 0644]
src/editor/plugins/core/edumed/edumed.less [new file with mode: 0644]
src/editor/plugins/core/edumed/order/element.js [new file with mode: 0644]
src/editor/plugins/core/edumed/order/order.less [new file with mode: 0644]
src/editor/plugins/core/edumed/order/view.html [new file with mode: 0644]
src/editor/plugins/core/edumed/order/view.js [new file with mode: 0644]
src/editor/plugins/core/edumed/order/viewItem.html [new file with mode: 0644]

index 6a663b1..b2e3151 100644 (file)
@@ -1,3 +1,5 @@
+@import 'edumed/edumed.less';
+
 .canvas-widget-footnote-handle {
     display: inline;
     outline: 0px solid transparent;
index e300232..6156b89 100644 (file)
@@ -11,8 +11,8 @@ var _ = require('libs/underscore'),
     plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}, documentNode: {}}},
     Dialog = require('views/dialog/dialog'),
     canvasElements = require('plugins/core/canvasElements'),
-    metadataEditor = require('plugins/core/metadataEditor/metadataEditor');
-
+    metadataEditor = require('plugins/core/metadataEditor/metadataEditor'),
+    edumed = require('plugins/core/edumed/edumed');
 
 
 plugin.documentExtension.textNode.transformations = {
@@ -590,8 +590,7 @@ plugin.actions = [
     createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
     linkAction,
     metadataEditor.action(metadataParams)
-].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions);
-
+].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
 
 
 plugin.config = function(config) {
@@ -608,7 +607,7 @@ plugin.config = function(config) {
     });
 };
 
-plugin.canvasElements = canvasElements;
+plugin.canvasElements = canvasElements.concat(edumed.canvasElements);
 
 return plugin;
 
diff --git a/src/editor/plugins/core/edumed/actions.js b/src/editor/plugins/core/edumed/actions.js
new file mode 100644 (file)
index 0000000..8b968b8
--- /dev/null
@@ -0,0 +1,52 @@
+define(function() {
+    
+'use strict';
+
+/* globals gettext */
+
+
+var createAction = function(actionConfig) {
+
+    return {
+        name: actionConfig.name,
+        params: {
+            fragment: {type: 'context', name: 'fragment'}
+        },
+        stateDefaults: {
+            icon: actionConfig.icon || null,
+            execute: function(callback, params) {
+                /* globals Node */
+                void(callback);
+
+                var node = params.fragment.node;
+                if(node.nodeType === Node.TEXT_NODE) {
+                    node = node.parent();
+                }
+
+                node.document.transaction(function() {
+                    var exerciseNode = node.after(node.document.edumedCreateExerciseNode(actionConfig.exercise));
+
+                    //@@
+                    if(actionConfig.exercise === 'order') {
+                        exerciseNode.object.addItem('first item');
+                        exerciseNode.contents('.p')[0].append({text: 'Write here...'});
+                    }
+                });
+                
+            }
+        },
+        getState: function(params) {
+            return {
+                allowed: params.fragment && params.fragment.isValid() && params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isInside('exercise'),
+                description: gettext('Insert exercise: ' + actionConfig.exerciseTitle)
+            };
+        }
+    };
+
+};
+
+return [
+    createAction({name: 'insertOrderExercise', icon: 'random', exercise: 'order', exerciseTitle: gettext('Order') })
+];
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/edumed/edumed.js b/src/editor/plugins/core/edumed/edumed.js
new file mode 100644 (file)
index 0000000..bb64da0
--- /dev/null
@@ -0,0 +1,13 @@
+define(function(require) {
+    
+'use strict';
+
+var actions = require('./actions'),
+    orderExerciseElement = require('./order/element');
+
+return {
+    actions: actions,
+    canvasElements: [orderExerciseElement]
+};
+
+});
\ No newline at end of file
diff --git a/src/editor/plugins/core/edumed/edumed.less b/src/editor/plugins/core/edumed/edumed.less
new file mode 100644 (file)
index 0000000..31a3f5b
--- /dev/null
@@ -0,0 +1,18 @@
+@import 'order/order.less';
+
+.edumed-exercise {
+
+    border: 1px lighten(#000, 35%) dashed;
+    margin: 15px 0;
+    padding: 10px;
+
+    .header {
+        font-weight: bold;
+        font-variant: small-caps;
+        margin-bottom: 10px;
+    }
+
+    .btn {
+        line-height: 12px;
+    }
+}
\ No newline at end of file
diff --git a/src/editor/plugins/core/edumed/order/element.js b/src/editor/plugins/core/edumed/order/element.js
new file mode 100644 (file)
index 0000000..7c5e383
--- /dev/null
@@ -0,0 +1,99 @@
+define(function(require) {
+    
+'use strict';
+
+/* globals gettext */
+
+var _ = require('libs/underscore');
+
+var documentElement = require('modules/documentCanvas/canvas/documentElement'),
+    OrderExerciseView = require('./view');
+
+var OrderExerciceElement = Object.create(documentElement.DocumentNodeElement.prototype);
+_.extend(OrderExerciceElement, {
+    init: function() {
+        documentElement.DocumentNodeElement.prototype.init.call(this);
+        
+        this.view = new OrderExerciseView(this, this.wlxmlNode);
+        this._container().append(this.view.dom);
+
+        this.view.on('newItemRequested', function() {
+            this.wlxmlNode.document.transaction(function() {
+                var textNode = this.wlxmlNode.object.addItem('');
+                var doc = this.wlxmlNode.document;
+                return doc.createFragment(doc.CaretFragment, {node: textNode, offset:0});
+            }.bind(this), {
+                metadata: {
+                    description: gettext('Add item to exercise')
+                },
+                success: function(ret) {
+                    this.canvas.select(ret);
+                }.bind(this)
+            });
+        }.bind(this));
+
+        this.view.on('moveAnswer', function(sourceItem, targetItem, where) {
+            this.wlxmlNode.document.transaction(function() {
+                sourceItem.setAnswer(targetItem.getAnswer() + (where === 'before' ? 0 : 1));
+            }, {
+                metadata: {
+                    description: gettext('Change solution')
+                }
+            });
+        }.bind(this));
+
+        this.view.on('moveItem', function(sourceItem, targetItem, where) {
+            this.wlxmlNode.document.transaction(function() {
+                targetItem.node[where](sourceItem.node);
+            }, {
+                metadata: {
+                    description: gettext('Change order')
+                }
+            });
+            
+        }.bind(this));
+
+        var exerciseNode = this.wlxmlNode;
+        this.createContainer(this.wlxmlNode.object.getDescription(), {
+            manages: function(node, removedFrom) {
+                if(node.is('list.orderable')) {
+                    return false;
+                }
+                return exerciseNode.sameNode(node.parent() || removedFrom); //!n.hasFollowingSibing(this.params.listnode);    
+            },
+            dom: this.view.dom.find('.description')
+        });
+
+        this.reloadView();
+    },
+    onNodeAdded: function(event) {
+        var node = event.meta.node;
+        if(this.wlxmlNode.object.isItemNode(node)) {
+            this.reloadView();
+        }
+    },
+    onNodeAttrChange: function(event) {
+        var node = event.meta.node;
+        if(node.is('item.answer') && node.parent() && node.parent().is('list.orderable')) {
+            this.reloadView();
+        }
+    },
+    reloadView: function() {
+        this.view.clearItems();
+        this.wlxmlNode.object.getItems().forEach(function(item) {
+            this.view.addItem(item);
+        }.bind(this));
+    },
+    getVerticallyFirstTextElement: function() {
+        var toret;
+        this.containers.some(function(container) {
+            toret = container.getVerticallyFirstTextElement();
+            return !!toret;
+        });
+        return toret;
+    }
+});
+
+return {tag: 'div', klass: 'exercise.order', prototype: OrderExerciceElement};
+
+});
diff --git a/src/editor/plugins/core/edumed/order/order.less b/src/editor/plugins/core/edumed/order/order.less
new file mode 100644 (file)
index 0000000..b407259
--- /dev/null
@@ -0,0 +1,66 @@
+.exercise-order {
+    
+    overflow-x: hidden;
+
+    .description {
+        margin-bottom: 20px;
+    }
+
+    .wrapper {
+        display: table;
+        width: 100%;
+        border-radius: 5px;
+        background-color: #ddd;
+        -webkit-margin-before: -20px; // compensate for Chrome behavior for ol > li > table
+    }
+
+    .content {
+        display: table-cell;
+        width: calc(~'100% - 40px');
+        padding: 10px 10px;
+        cursor: default;
+
+        .dragEnter {
+            opacity: 0.5;
+        }
+    }
+
+    .handle {
+        vertical-align: middle;
+        display: table-cell;
+        width: 20px;
+        background-color: darken(#ddd, 15%);
+        text-align: center;
+        border-radius: 0 5px 5px 0;
+        padding: 10px 10px;
+        pointer-events: none;
+
+        .dragEnter {
+            opacity: 0.5;
+        }
+    }
+
+    .placeholder {
+        height: 10px;
+        transition: height 0.05s;
+        background-color: white;
+        &.active {
+            height: 60px;
+            transition: height 0.05s;
+            background-color: lighten(@blue,40%);
+            border: 1px dashed @blue;
+        }
+        width:100%;
+    }
+
+    .placeholder-top {
+        margin-left: 25px;
+    }
+
+    li {
+        cursor: move;
+        .dragged {
+            opacity: 0.6;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/editor/plugins/core/edumed/order/view.html b/src/editor/plugins/core/edumed/order/view.html
new file mode 100644 (file)
index 0000000..492b8f4
--- /dev/null
@@ -0,0 +1,10 @@
+<div class="edumed-exercise exercise-order">
+    <div class="header"><%= gettext('Exercise') %></div>
+    <div class="description"></div>
+    <ul class="nav nav-tabs modePills">
+        <li class="active" mode="initial"><a href="#"><%= gettext('Initial') %></a></li>
+        <li mode="correct"><a href="#"><%= gettext('Solution') %></a></li>
+    </ul>
+    <div class="placeholder placeholder-top"></div>
+    <ol></ol>
+</div>
\ No newline at end of file
diff --git a/src/editor/plugins/core/edumed/order/view.js b/src/editor/plugins/core/edumed/order/view.js
new file mode 100644 (file)
index 0000000..2b824cf
--- /dev/null
@@ -0,0 +1,212 @@
+define(function(require) {
+    
+'use strict';
+var $ = require('libs/jquery');
+
+var _ = require('libs/underscore'),
+    Backbone = require('libs/backbone'),
+    viewTemplate = require('libs/text!./view.html'),
+    viewItemTemplate = require('libs/text!./viewItem.html');
+
+
+var OrderExerciseView = function(element) {
+    this.element = element;
+    this.dom = $(_.template(viewTemplate)());
+    this.modePills = this.dom.find('.modePills');
+    this.list = this.dom.find('ol');
+    this.addButton = this.dom.find('button.add');
+    this.description = this.dom.find('.description');
+    this.itemViews = [];
+
+    this.addButton.on('click', function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+        this.trigger('newItemRequested');
+        //_.last(this.itemViews).editStart();
+    }.bind(this));
+
+    this.modePills.find('a').on('click', function(e) {
+        e.stopPropagation();
+        e.preventDefault();
+        this.setMode($(e.target).parent().attr('mode'));
+    }.bind(this));
+
+    this.mode = 'initial';
+
+    var dropTargets = this.dom.find('.placeholder-top');
+
+    dropTargets.on('dragend', function() {
+        dropTargets.removeClass('dragged');
+    });
+
+    dropTargets.on('dragenter', function() {
+        var first = this.itemViews[0];
+        if(this.mode === 'correct') {
+            first = this.itemViews.slice(0)
+                .sort(function(view1, view2) {
+                    if(view1.item.getAnswer() > view2.item.getAnswer()) {
+                        return 1;
+                    }
+                    return -1;
+                })[0];
+        }
+        if(!this.allowDropAt(first, true)) {
+            return;
+        }
+        dropTargets.addClass('active');
+    }.bind(this));
+
+    dropTargets.on('dragleave', function() {
+        dropTargets.removeClass('active');
+    }.bind(this));
+
+    dropTargets.on('dragover', function(e) {
+        e.preventDefault();
+        e.originalEvent.dataTransfer.dropEffect = 'move';
+    });
+
+    dropTargets.on('drop', function(e) {
+        var vid = e.originalEvent.dataTransfer.getData('text');
+        var droppedItem = $('[vid='+vid+']').data('viewInstance');
+
+        var first = this.itemViews[0];
+        if(this.mode === 'correct') {
+            first = this.itemViews.slice(0)
+                .sort(function(view1, view2) {
+                    if(view1.item.getAnswer() > view2.item.getAnswer()) {
+                        return 1;
+                    }
+                    return -1;
+                })[0];
+        }
+
+        this.trigger(this.mode === 'initial' ? 'moveItem' : 'moveAnswer', droppedItem.item, first.item, 'before');
+        dropTargets.removeClass('active');
+    }.bind(this));
+};
+_.extend(OrderExerciseView.prototype, Backbone.Events, {
+    addItem: function(item) {
+        var view = new ItemView(item, this);
+        view.on('edit', function(text) {
+            this.trigger('itemEdited', item, text);
+        }.bind(this));
+        view.on('receivedDrop', function(droppedItem) {
+            this.trigger(this.mode === 'initial' ? 'moveItem' : 'moveAnswer', droppedItem.item, item, 'after');
+        }.bind(this));
+        view.on('dragStarted', function(view) {
+            this.draggedView = view;
+        }.bind(this));
+        this.list.append(view.dom);
+        this.itemViews.push(view);
+
+        if(this.mode === 'correct') {
+            this.setMode(this.mode);
+        }
+    },
+    clearItems: function() {
+        this.list.empty();
+        this.itemViews.forEach(function(view) {
+            view.remove();
+        });
+        this.itemViews = [];
+    },
+    setMode: function(mode) {
+        this.modePills.find('li').removeClass('active');
+        this.modePills.find('[mode='+mode+']').addClass('active');
+        this.mode = mode;
+        this.list.children().detach();
+
+        if(this.mode === 'initial') {
+            this.itemViews.forEach(function(itemView) {
+                this.list.append(itemView.dom);
+            }.bind(this));
+        } else {
+            this.itemViews.slice(0)
+                .sort(function(view1, view2) {
+                    if(view1.item.getAnswer() > view2.item.getAnswer()) {
+                        return 1;
+                    }
+                    return -1;
+                })
+                .forEach(function(itemView) {
+                    this.list.append(itemView.dom);
+                }.bind(this));
+        }
+    },
+    allowDropAt: function(view, up) {
+        var arr = [this.draggedView.dom[0]];
+        if(!up) {
+            arr.push(this.draggedView.dom.prev()[0]);
+        }
+        return !_.contains(arr, view.dom[0]);
+    }
+});
+
+var ItemView = function(item, exerciseView) {
+    this.item = item;
+    this.exerciseView = exerciseView;
+    this.dom = $(_.template(viewItemTemplate)());
+    this.content = this.dom.find('.content');
+
+
+    var dropTargets = this.dom.find('.placeholder'),
+        dragSources = this.dom.find('.wrapper');
+
+    dragSources.on('dragstart', function(e) {
+        this.dom.addClass('dragged');
+        e.originalEvent.dataTransfer.setData('text', this.dom.attr('vid'));
+        e.originalEvent.effectAllowed = 'move';
+        this.trigger('dragStarted', this);
+
+    }.bind(this));
+
+    dropTargets.on('dragend', function() {
+        this.dom.removeClass('dragged');
+    });
+
+    dropTargets.on('dragenter', function() {
+        if(!this.exerciseView.allowDropAt(this)) {
+            return;
+        }
+        dropTargets.addClass('active');
+    }.bind(this));
+
+    dropTargets.on('dragleave', function() {
+        dropTargets.removeClass('active');
+    }.bind(this));
+
+    dropTargets.on('dragover', function(e) {
+        e.preventDefault();
+        e.originalEvent.dataTransfer.dropEffect = 'move';
+    });
+
+    dropTargets.on('drop', function(e) {
+        var vid = e.originalEvent.dataTransfer.getData('text');
+        var droppedItem = $('[vid='+vid+']').data('viewInstance');
+        e.preventDefault();
+        this.trigger('receivedDrop', droppedItem);
+    }.bind(this));
+
+    var content = this.content;
+    this.container = exerciseView.element.createContainer(item.node.contents(), {
+        manages: function(node, originaParent) {
+            return item.node.sameNode(node.parent() || originaParent);
+        },
+        dom: content
+    });
+
+    this.dom.data('viewInstance', this);
+    this.dom.attr('vid', _.uniqueId());
+};
+
+_.extend(ItemView.prototype, Backbone.Events, {
+    remove: function() {
+        this.container.remove();
+    }
+});
+
+return OrderExerciseView;
+
+});
+
+
diff --git a/src/editor/plugins/core/edumed/order/viewItem.html b/src/editor/plugins/core/edumed/order/viewItem.html
new file mode 100644 (file)
index 0000000..1e9b1b1
--- /dev/null
@@ -0,0 +1,9 @@
+<li>
+    <div class="wrapper" draggable="true">
+        <div class="content"></div>
+        <div class="handle">
+            <i class="icon-resize-vertical icon-white"></i>
+        </div>
+    </div>
+    <div class="placeholder"></div>
+</li>
\ No newline at end of file