From 856f00d24d37590a78b78833a8aa305983e58e45 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Aleksander=20=C5=81ukasz?= Date: Tue, 5 Aug 2014 11:14:46 +0200 Subject: [PATCH 1/1] editor: core plugin - edumed - first take on order exercise --- src/editor/plugins/core/canvasElements.less | 2 + src/editor/plugins/core/core.js | 9 +- src/editor/plugins/core/edumed/actions.js | 52 +++++ src/editor/plugins/core/edumed/edumed.js | 13 ++ src/editor/plugins/core/edumed/edumed.less | 18 ++ .../plugins/core/edumed/order/element.js | 99 ++++++++ .../plugins/core/edumed/order/order.less | 66 ++++++ .../plugins/core/edumed/order/view.html | 10 + src/editor/plugins/core/edumed/order/view.js | 212 ++++++++++++++++++ .../plugins/core/edumed/order/viewItem.html | 9 + 10 files changed, 485 insertions(+), 5 deletions(-) create mode 100644 src/editor/plugins/core/edumed/actions.js create mode 100644 src/editor/plugins/core/edumed/edumed.js create mode 100644 src/editor/plugins/core/edumed/edumed.less create mode 100644 src/editor/plugins/core/edumed/order/element.js create mode 100644 src/editor/plugins/core/edumed/order/order.less create mode 100644 src/editor/plugins/core/edumed/order/view.html create mode 100644 src/editor/plugins/core/edumed/order/view.js create mode 100644 src/editor/plugins/core/edumed/order/viewItem.html diff --git a/src/editor/plugins/core/canvasElements.less b/src/editor/plugins/core/canvasElements.less index 6a663b1..b2e3151 100644 --- a/src/editor/plugins/core/canvasElements.less +++ b/src/editor/plugins/core/canvasElements.less @@ -1,3 +1,5 @@ +@import 'edumed/edumed.less'; + .canvas-widget-footnote-handle { display: inline; outline: 0px solid transparent; diff --git a/src/editor/plugins/core/core.js b/src/editor/plugins/core/core.js index e300232..6156b89 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -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 index 0000000..8b968b8 --- /dev/null +++ b/src/editor/plugins/core/edumed/actions.js @@ -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 index 0000000..bb64da0 --- /dev/null +++ b/src/editor/plugins/core/edumed/edumed.js @@ -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 index 0000000..31a3f5b --- /dev/null +++ b/src/editor/plugins/core/edumed/edumed.less @@ -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 index 0000000..7c5e383 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/element.js @@ -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 index 0000000..b407259 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/order.less @@ -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 index 0000000..492b8f4 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/view.html @@ -0,0 +1,10 @@ +
+
<%= gettext('Exercise') %>
+
+ +
+
    +
    \ 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 index 0000000..2b824cf --- /dev/null +++ b/src/editor/plugins/core/edumed/order/view.js @@ -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 index 0000000..1e9b1b1 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/viewItem.html @@ -0,0 +1,9 @@ +
  1. +
    +
    +
    + +
    +
    +
    +
  2. \ No newline at end of file -- 2.20.1