From: Aleksander Ɓukasz Date: Thu, 14 Aug 2014 14:26:42 +0000 (+0200) Subject: Merge in first approach to edumed exercises X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/8411d228c0928c0a2d6c280a2f39adacc972ee13?hp=c8101069e6de3c7eeb44e97e391c9fa4b9db0872 Merge in first approach to edumed exercises --- diff --git a/karma.conf.js b/karma.conf.js index 9f84f89..913c295 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -9,6 +9,7 @@ files = [ {pattern: 'libs/*.js', included: false}, {pattern: 'src/**/*.js', included: false}, {pattern: 'src/**/*.html', included: false}, + {pattern: 'src/**/*.xml', included: false}, 'tests/main.js', ]; diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index 0259452..ebede40 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -3,6 +3,7 @@ define([ 'libs/underscore', 'libs/backbone', 'fnpjs/logging/logging', +'views/menu/menu', 'modules/documentCanvas/canvas/documentElement', 'modules/documentCanvas/canvas/keyboard', 'modules/documentCanvas/canvas/utils', @@ -14,8 +15,8 @@ define([ 'modules/documentCanvas/canvas/selection', 'modules/documentCanvas/canvas/keyEvent', 'libs/text!./canvas.html' -], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement, nullElement, gutter, selection, keyEvent, canvasTemplate) { - +], function($, _, Backbone, logging, Menu, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement, nullElement, gutter, selection, keyEvent, canvasTemplate) { + 'use strict'; /* global document:false, window:false, Node:false, gettext */ @@ -62,9 +63,10 @@ $.extend(TextHandler.prototype, { }); -var Canvas = function(wlxmlDocument, elements, metadata) { +var Canvas = function(wlxmlDocument, elements, metadata, sandbox) { this.metadata = metadata || {}; - this.elementsRegister = new ElementsRegister(documentElement.DocumentNodeElement, nullElement); + this.sandbox = sandbox; + this.elementsRegister = this.createElementsRegister(); elements = [ {tag: 'section', klass: null, prototype: genericElement}, @@ -95,6 +97,15 @@ var Canvas = function(wlxmlDocument, elements, metadata) { $.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; }, @@ -109,14 +120,28 @@ $.extend(Canvas.prototype, Backbone.Events, { this.reloadRoot(); }, - createElement: function(wlxmlNode) { + createElement: function(wlxmlNode, register, useRoot) { var Factory; + register = register || this.elementsRegister; if(wlxmlNode.nodeType === Node.TEXT_NODE) { Factory = documentElement.DocumentTextElement; } else { + Factory = register.getElement({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()}); + } + if(!Factory && useRoot) { Factory = this.elementsRegister.getElement({tag: wlxmlNode.getTagName(), klass: wlxmlNode.getClass()}); + if(!Factory) { + Factory = documentElement.DocumentNodeElement; + } } - return new Factory(wlxmlNode, this); + + if(Factory) { + return new Factory(wlxmlNode, this); + } + }, + + createElementsRegister: function() { + return new ElementsRegister(documentElement.DocumentNodeElement, nullElement); }, getDocumentElement: function(htmlElement) { @@ -147,7 +172,6 @@ $.extend(Canvas.prototype, Backbone.Events, { this.rootWrapper.append(this.rootElement.dom); }, - triggerKeyEvent: function(keyEvent, selection) { selection = selection || this.getSelection(); if(selection && (selection.type === 'caret' || selection.type === 'textSelection') && selection.toDocumentFragment().isValid()) { @@ -155,6 +179,10 @@ $.extend(Canvas.prototype, Backbone.Events, { } }, + createAction: function(fqName, config) { + return this.sandbox.createAction(fqName, config); + }, + setupEventHandling: function() { var canvas = this; @@ -196,6 +224,17 @@ $.extend(Canvas.prototype, Backbone.Events, { } }); + this.rootWrapper.on('contextmenu', function(e) { + var el = canvas.getDocumentElement(e.target); + + if(!el) { + return; + } + + e.preventDefault(); + this.showContextMenu(el, {x: e.clientX, y: e.clientY}); + }.bind(this)); + this.rootWrapper.on('paste', function(e) { e.preventDefault(); @@ -454,6 +493,18 @@ $.extend(Canvas.prototype, Backbone.Events, { if(position.element) { this._moveCaretToTextElement(position.element, position.offset); } + }, + showContextMenu: function(element, coors) { + var menu = new Menu(); + + while(element) { + (element.contextMenuActions || []).forEach(menu.addAction.bind(menu)); + element = element.parent(); + } + if(menu.actions.length) { + menu.updateContextParam('fragment', this.getSelection().toDocumentFragment()); + this.sandbox.showContextMenu(menu, {x: coors.x, y: coors.y}); + } } }); @@ -585,8 +636,8 @@ $.extend(Cursor.prototype, { }); return { - fromXMLDocument: function(wlxmlDocument, elements, metadata) { - return new Canvas(wlxmlDocument, elements, metadata); + fromXMLDocument: function(wlxmlDocument, elements, metadata, sandbox) { + return new Canvas(wlxmlDocument, elements, metadata, sandbox); } }; diff --git a/src/editor/modules/documentCanvas/canvas/container.js b/src/editor/modules/documentCanvas/canvas/container.js index 6436526..663a159 100644 --- a/src/editor/modules/documentCanvas/canvas/container.js +++ b/src/editor/modules/documentCanvas/canvas/container.js @@ -8,13 +8,15 @@ var $ = require('libs/jquery'), var Container = function(nodes, params, element) { + params = params || {}; _.extend(this, params); this.dom = this.dom || $('
'); this.dom.addClass('canvas-container'); + this.dom.toggleClass('reset-background', !!params.resetBackground); this.element = element; nodes.forEach(function(node) { - var el = this.element.canvas.createElement(node); + var el = this.element.createElement(node); if(el.dom) { this.dom.append(el.dom); } @@ -102,7 +104,7 @@ _.extend(Container.prototype, { if(param instanceof documentElement.DocumentElement) { element = param; } else { - element = this.element.canvas.createElement(param);// + element = this.element.createElement(param);// } if(element.dom) { this.dom.prepend(element.dom); diff --git a/src/editor/modules/documentCanvas/canvas/container.less b/src/editor/modules/documentCanvas/canvas/container.less new file mode 100644 index 0000000..dc4a514 --- /dev/null +++ b/src/editor/modules/documentCanvas/canvas/container.less @@ -0,0 +1,5 @@ +.canvas-container { + .reset-background { + background-color: @white; + } +} diff --git a/src/editor/modules/documentCanvas/canvas/documentElement.js b/src/editor/modules/documentCanvas/canvas/documentElement.js index 859a80b..a1965cd 100644 --- a/src/editor/modules/documentCanvas/canvas/documentElement.js +++ b/src/editor/modules/documentCanvas/canvas/documentElement.js @@ -95,6 +95,8 @@ $.extend(DocumentElement.prototype, { var DocumentNodeElement = function(wlxmlNode, canvas) { DocumentElement.call(this, wlxmlNode, canvas); this.containers = []; + this.elementsRegister = canvas.createElementsRegister(); + this.contextMenuActions = []; this.init(this.dom); }; @@ -104,7 +106,7 @@ var manipulate = function(e, params, action) { if(params instanceof DocumentElement) { element = params; } else { - element = e.canvas.createElement(params); + element = e.createElement(params); } if(element.dom) { e.dom[action](element.dom); @@ -146,6 +148,13 @@ $.extend(DocumentNodeElement.prototype, { this.containers.splice(idx, 1); } }, + createElement: function(wlxmlNode) { + var parent = this.wlxmlNode.parent() ? utils.getElementForNode(this.wlxmlNode.parent()) : null; + return this.canvas.createElement(wlxmlNode, this.elementsRegister, !parent) || parent.createElement(wlxmlNode); + }, + addToContextMenu: function(actionFqName) { + this.contextMenuActions.push(this.canvas.createAction(actionFqName)); + }, handle: function(event) { var method = 'on' + event.type[0].toUpperCase() + event.type.substr(1), target; @@ -232,6 +241,9 @@ $.extend(DocumentNodeElement.prototype, { // }) this.dom.css('display', what); this._container().css('display', what); + }, + children: function() { + return []; } }); @@ -293,7 +305,7 @@ $.extend(DocumentTextElement.prototype, { if(params instanceof DocumentNodeElement) { element = params; } else { - element = this.canvas.createElement(params); + element = this.parent().createElement(params); } if(element.dom) { this.dom.wrap('
'); @@ -311,7 +323,7 @@ $.extend(DocumentTextElement.prototype, { if(params instanceof DocumentNodeElement) { element = params; } else { - element = this.canvas.createElement(params); + element = this.createElement(params); } if(element.dom) { this.dom.wrap('
'); diff --git a/src/editor/modules/documentCanvas/canvas/elementsRegister.js b/src/editor/modules/documentCanvas/canvas/elementsRegister.js index 9a44514..7b2cf99 100644 --- a/src/editor/modules/documentCanvas/canvas/elementsRegister.js +++ b/src/editor/modules/documentCanvas/canvas/elementsRegister.js @@ -38,9 +38,6 @@ _.extend(ElementsRegister.prototype, { } }.bind(this)); } - if(!Factory) { - Factory = this.BaseType; - } return Factory; } }); diff --git a/src/editor/modules/documentCanvas/canvas/genericElement.js b/src/editor/modules/documentCanvas/canvas/genericElement.js index 8ed08bf..fd0dfc4 100644 --- a/src/editor/modules/documentCanvas/canvas/genericElement.js +++ b/src/editor/modules/documentCanvas/canvas/genericElement.js @@ -37,7 +37,7 @@ $.extend(generic, { }, refresh: function() { - if(this.wlxmlNode.getTagName() === 'span') { + if(this.wlxmlNode.getTagName() === 'span' || this.wlxmlNode.getTagName() === 'aside') { if(this.containsBlock()) { this.displayAsBlock(); } else { diff --git a/src/editor/modules/documentCanvas/canvas/keyboard.js b/src/editor/modules/documentCanvas/canvas/keyboard.js index 6b32da0..e43ea89 100644 --- a/src/editor/modules/documentCanvas/canvas/keyboard.js +++ b/src/editor/modules/documentCanvas/canvas/keyboard.js @@ -257,6 +257,14 @@ var keyEventHandlers = [ return; } + + var parent = element.wlxmlNode.parent(); + if(element.wlxmlNode.getIndex() === 0 && parent.isContextRoot() && (!parent.is('item') || parent.getIndex() === 0)) { + // Don't even try to do anything at the edge of a context root, except for non-first items + // - this is a temporary solution until key events handling get refactored into something more sane. + return; + } + e.preventDefault(); s.canvas.wlxmlDocument.transaction(function() { @@ -348,14 +356,44 @@ var keyEventHandlers = [ } }, + { + applies: function(e, s) { + var parent = s.element && s.element.wlxmlNode.parent(), + parentIsItem = parent && parent.is('item'), + itemIsOnList = parent && parent.parent() && parent.parent().is('list'); + return s.type === 'caret' && e.key === KEYS.ENTER && s.element.isEmpty() && parentIsItem && itemIsOnList; + }, + run: function(e, s) { + var item = s.element.wlxmlNode.parent(), + list = item.parent(); + e.preventDefault(); + s.canvas.wlxmlDocument.transaction(function() { + var p = list.after({tagName: 'div', attrs: {'class': 'p'}}); + p.append({text: ''}); + item.detach(); + return p; + }, { + success: function(p) { + s.canvas.setCurrentElement(p); + } + }); + } + }, { applies: function(e, s) { return s.type === 'caret' && e.key === KEYS.ENTER && !s.element.parent().isRootElement(); }, run: function(e, s) { - var result, goto, gotoOptions; + var parent = s.element.parent(), + children = parent.children(), + result, goto, gotoOptions; void(e); e.preventDefault(); + + if(children.length === 1 && s.element.isEmpty()) { + return; + } + s.canvas.wlxmlDocument.transaction(function() { result = s.element.wlxmlNode.breakContent({offset: s.offset}); }, { diff --git a/src/editor/modules/documentCanvas/canvas/selection.js b/src/editor/modules/documentCanvas/canvas/selection.js index 4de392b..0d8c4ce 100644 --- a/src/editor/modules/documentCanvas/canvas/selection.js +++ b/src/editor/modules/documentCanvas/canvas/selection.js @@ -60,7 +60,8 @@ $.extend(TextSelection.prototype, { toDocumentFragment: function() { var doc = this.canvas.wlxmlDocument, anchorNode = this.anchorElement ? this.anchorElement.wlxmlNode : null, - focusNode = this.focusElement ? this.focusElement.wlxmlNode : null; + focusNode = this.focusElement ? this.focusElement.wlxmlNode : null, + node1, node2; if(!anchorNode || !focusNode) { return; @@ -75,10 +76,17 @@ $.extend(TextSelection.prototype, { }); } else { - var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode}); + if(anchorNode.hasSameContextRoot(focusNode)) { + var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode}); + node1 = siblingParents.node1; + node2 = siblingParents.node2; + } else { + node1 = focusNode; + node2 = anchorNode; + } return doc.createFragment(doc.RangeFragment, { - node1: siblingParents.node1, - node2: siblingParents.node2 + node1: node1, + node2: node2 }); } }, @@ -129,7 +137,7 @@ return { params = { type: 'caret', element: element, - offset: element.isEmpty() ? 0 : nativeSelection.focusOffset + offset: element.isEmpty() ? 0 : nativeSelection.getRangeAt(0).startOffset }; } else if(isText(nativeSelection.focusNode) && isText(nativeSelection.anchorNode)) { anchorElement = canvas.getDocumentElement(nativeSelection.anchorNode); diff --git a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js index d107612..b1a9582 100644 --- a/src/editor/modules/documentCanvas/canvas/wlxmlListener.js +++ b/src/editor/modules/documentCanvas/canvas/wlxmlListener.js @@ -49,7 +49,7 @@ var handlers = { if(event.meta.node.isRoot()) { this.canvas.reloadRoot(); } else { - newElement = this.canvas.createElement(event.meta.node); + newElement = element.parent().createElement(event.meta.node); element.dom.replaceWith(newElement.dom); } } diff --git a/src/editor/modules/documentCanvas/documentCanvas.js b/src/editor/modules/documentCanvas/documentCanvas.js index a98fdb4..a440bb9 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.js +++ b/src/editor/modules/documentCanvas/documentCanvas.js @@ -22,7 +22,7 @@ return function(sandbox) { var canvas = canvas3.fromXMLDocument(null, canvasElements, { user: sandbox.getConfig().user - }); + }, sandbox); var canvasWrapper = $(template); var shownAlready = false; var scrollbarPosition = 0, diff --git a/src/editor/modules/documentCanvas/documentCanvas.less b/src/editor/modules/documentCanvas/documentCanvas.less index 3d335dc..9677691 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.less +++ b/src/editor/modules/documentCanvas/documentCanvas.less @@ -1,5 +1,6 @@ @import 'nodes.less'; @import 'canvas/canvas.less'; +@import 'canvas/container.less'; @import 'canvas/documentElement.less'; @import 'canvas/genericElement.less'; @import 'canvas/gutter.less'; diff --git a/src/editor/modules/documentCanvas/nodes.less b/src/editor/modules/documentCanvas/nodes.less index 7938911..35ac7a8 100644 --- a/src/editor/modules/documentCanvas/nodes.less +++ b/src/editor/modules/documentCanvas/nodes.less @@ -142,22 +142,4 @@ [wlxml-tag="aside"] { margin-top: 10px; margin-bottom: 10px; - - [wlxml-class='gap'] { - display: inline; - } } - -[wlxml-class="gap"] { - &:before, &:after { - color: darken(@blue, 10%); - font-weight: bold; - //vertical-align: super; - } - &:before { - content: "("; - } - &:after { - content: ")"; - } -} \ No newline at end of file diff --git a/src/editor/modules/documentToolbar/documentToolbar.less b/src/editor/modules/documentToolbar/documentToolbar.less index 4b35b12..976fee4 100644 --- a/src/editor/modules/documentToolbar/documentToolbar.less +++ b/src/editor/modules/documentToolbar/documentToolbar.less @@ -26,10 +26,13 @@ .toolbar-widget { display: inline-block; - margin: 0 5px; + margin: 0; div { display: inline-block; } + .btn { + border-radius: 0; + } } } 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..31ac09d 100644 --- a/src/editor/plugins/core/core.js +++ b/src/editor/plugins/core/core.js @@ -11,9 +11,30 @@ 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'); + + +var exerciseFix = function(newNodes) { + var list, exercise, max, addedItem, answerValues; + if(newNodes.created.is('item')) { + list = newNodes.created.parent(); + exercise = list.parent(); + if(exercise && exercise.is('exercise')) { + if(exercise.is('exercise.order')) { + answerValues = exercise.object.getItems() + .map(function(item) { + if(!addedItem && item.node.sameNode(newNodes.created)) { + addedItem = item; + } + return item.getAnswer(); + }); + max = Math.max.apply(Math.max, answerValues); + addedItem.setAnswer(max + 1); + } + } + } +}; plugin.documentExtension.textNode.transformations = { breakContent: { @@ -38,6 +59,21 @@ plugin.documentExtension.textNode.transformations = { return true; // break } }); + + /* + /* + This makes sure that adding a new item to the list in some of the edumed exercises + sets an answer attribute that makes sense (and not just copies it which would create + a duplicate value). + + This won't be neccessary when/if we introduce canvas element own key event handlers. + + Alternatively, WLXML elements could implement their own item split methods that we + would delegate to. + */ + exerciseFix(newNodes); + /* */ + parentDescribingNodes.forEach(function(node) { newNodes.first.append(node); }); @@ -185,7 +221,7 @@ plugin.documentExtension.documentNode.transformations = { return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === ''); }, run: function() { - if(prev && prev.is('p') || prev.is({tagName: 'header'})) { + if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) { return merge(toMerge, prev); } if(prev && prev.is('list')) { @@ -224,7 +260,16 @@ plugin.documentExtension.documentNode.transformations = { } else if(prev.is({tagName: 'span'})) { if((txtNode = prev.getLastTextNode())) { txt = txtNode.getText(); - txtNode.setText(txt.substr(0, txt.length-1)); + if(txt.length > 1) { + txtNode.setText(txt.substr(0, txt.length-1)); + } else { + if(txtNode.parent().contents().length === 1) { + txtNode.parent().detach(); + } else { + txtNode.detach(); + } + + } return toret; } } @@ -590,8 +635,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 +652,7 @@ plugin.config = function(config) { }); }; -plugin.canvasElements = canvasElements; +plugin.canvasElements = canvasElements.concat(edumed.canvasElements); return plugin; diff --git a/src/editor/plugins/core/core.test.js b/src/editor/plugins/core/core.test.js index 00fc421..27a7254 100644 --- a/src/editor/plugins/core/core.test.js +++ b/src/editor/plugins/core/core.test.js @@ -546,7 +546,7 @@ describe('Keyboard interactions', function() { expect(selection.element.sameNode(getTextElement('has a cat', c))).to.equal(true); expect(selection.offset).to.equal(0); }); - it('deletes span if it contains only one character', function() { + it('deletes span if it contains only one character (1)', function() { var c = getCanvasFromXML('
Alice h a cat
'), k = new Keyboard(c); @@ -561,6 +561,21 @@ describe('Keyboard interactions', function() { expect(selection.element.sameNode(getTextElement('Alice a cat', c))).to.equal(true); expect(selection.offset).to.equal(6); }); + it('deletes span if it contains only one character (2)', function() { + var c = getCanvasFromXML('
ab
'), + k = new Keyboard(c); + + k.withCaret('|b').press(K.BACKSPACE); + + var rootContents = c.wlxmlDocument.root.contents(); + expect(rootContents.length).to.equal(1); + expect(rootContents[0].contents()[0].getText()).to.equal('b'); + + var selection = c.getSelection(); + expect(selection.type).to.equal('caret'); + expect(selection.element.sameNode(getTextElement('b', c))).to.equal(true); + expect(selection.offset).to.equal(0); + }); }); describe('splitting with enter', function() { @@ -621,6 +636,17 @@ describe('Keyboard interactions', function() { expect(selection.offset).to.equal(0); }); + it('does nothing on an empty paragraph', function() { + var c = getCanvasFromXML('
a
'), + k = new Keyboard(c), + spy = sinon.spy(); + + k.withCaret('a|').press(K.BACKSPACE); + c.wlxmlDocument.on('change', spy); + k.press(K.ENTER); + expect(spy.callCount).to.equal(0); + }); + it('splits its parent box if inside a span', function() { var c = getCanvasFromXML('
this is a paragraph
'), k = new Keyboard(c); @@ -700,6 +726,29 @@ describe('Keyboard interactions', function() { }); }); + describe('Enter on a list items', function() { + afterEach(removeCanvas); + + it('creates a paragraph after a list if hitting enter on the last and empty list item', function() { + var c = getCanvasFromXML('
item
'), + k = new Keyboard(c); + + k.withCaret('item|').press(K.ENTER).press(K.ENTER); + + var rootContents = c.wlxmlDocument.root.contents(); + expect(rootContents.length).to.equal(2); + expect(rootContents[0].is('list')).to.equal(true); + expect(rootContents[1].is('p')).to.equal(true); + + var list = rootContents[0]; + expect(list.contents().length).to.equal(1); + + var selection = c.getSelection(); + expect(selection.element.wlxmlNode.sameNode(rootContents[1].contents()[0])).to.equal(true); + expect(selection.offset).to.equal(0); + }); + }); + describe('Deleting text from a node', function() { it('deletes last character with backspace', function() { var c = getCanvasFromXML('
a
b
'), diff --git a/src/editor/plugins/core/edumed/actions.js b/src/editor/plugins/core/edumed/actions.js new file mode 100644 index 0000000..b79d109 --- /dev/null +++ b/src/editor/plugins/core/edumed/actions.js @@ -0,0 +1,80 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var _ = require('libs/underscore'); + +var descriptionText = gettext('Description goes here') + '...', + firstItemText = gettext('First item') + '...'; + +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(); + } + if(node.isInside('exercise')) { + node = node.getParent('exercise'); + } + + node.document.transaction(function() { + var exerciseNode = node.after(node.document.edumedCreateExerciseNode(actionConfig.exercise)), + doc = params.fragment.document, + cursorTarget; + + //@@ + if(actionConfig.exercise === 'order') { + exerciseNode.object.addItem(firstItemText); + cursorTarget = exerciseNode.contents('.p')[0].append({text: descriptionText}); + } else if(_.contains(['gap', 'replace'], actionConfig.exercise)) { + cursorTarget = _.first(exerciseNode.contents('.p')); + if(cursorTarget) { + cursorTarget = cursorTarget.append({text: ''}); + } + } else if(actionConfig.exercise.split('.')[0] === 'choice') { + _.first(exerciseNode.find('p')).append({text: descriptionText}); + cursorTarget = _.first(exerciseNode.find('item.answer')); + if(cursorTarget) { + cursorTarget = cursorTarget.append({text: ''}); + } + } + if(cursorTarget) { + callback(doc.createFragment(doc.CaretFragment, {node: cursorTarget, offset: cursorTarget.getText().length})); + } + }); + + } + }, + getState: function(params) { + return { + allowed: params.fragment && params.fragment.isValid() && params.fragment instanceof params.fragment.NodeFragment, + description: gettext('Insert exercise') + ': ' + actionConfig.exerciseTitle + }; + } + }; + +}; + +return [ + createAction({name: 'insertOrderExercise', icon: 'random', exercise: 'order', exerciseTitle: gettext('Order')}), + createAction({name: 'insertChoiceSingleExercise', icon: 'ok-circle', exercise: 'choice.single', exerciseTitle: gettext('Single Choice')}), + createAction({name: 'insertChoiceMultiExercise', icon: 'check', exercise: 'choice', exerciseTitle: gettext('Multiple Choice')}), + createAction({name: 'insertChoiceTrueOrFalseExercise', icon: 'adjust', exercise: 'choice.true-or-false', exerciseTitle: gettext('True or False')}), + createAction({name: 'insertGapsExercise', icon: 'question-sign', exercise: 'gap', exerciseTitle: gettext('Gaps')}), + createAction({name: 'insertReplaceExercise', icon: 'thumbs-down', exercise: 'replace', exerciseTitle: gettext('Replace')}) +]; + +}); 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..6a80f87 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choice.less @@ -0,0 +1,3 @@ +@import 'choiceTrueOrFalse.less'; +@import 'choiceSingle.less'; +@import 'choiceMulti.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..aeafed4 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceBase.js @@ -0,0 +1,62 @@ +define(function(require) { + +'use strict'; + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + elementBase = require('plugins/core/edumed/elementBase'), + template = require('libs/text!./element.html'); + + +var choiceBase = Object.create(elementBase); +_.extend(choiceBase, { + init: function() { + elementBase.init.call(this); + this.view = $(_.template(template)({type: this.type, name: this.name})); + this._container().append(this.view); + + this.createContainer(this.wlxmlNode.contents().filter(function(n) {return !n.is('list');}), { + resetBackground: true, + 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..0983930 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceMulti.js @@ -0,0 +1,50 @@ +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', + name: gettext('Multiple Choice'), + 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/choiceMulti.less b/src/editor/plugins/core/edumed/choice/choiceMulti.less new file mode 100644 index 0000000..92877b2 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceMulti.less @@ -0,0 +1,11 @@ +.exercise-multi { + input { + margin-right: 15px; + } + ul { + margin: 0; + } + li { + list-style-type: none; + } +} \ 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..68ff576 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceSingle.js @@ -0,0 +1,57 @@ +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', + name: gettext('Single Choice'), + 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/choiceSingle.less b/src/editor/plugins/core/edumed/choice/choiceSingle.less new file mode 100644 index 0000000..ddb9c96 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceSingle.less @@ -0,0 +1,11 @@ +.exercise-single { + input { + margin-right: 15px; + } + ul { + margin: 0; + } + li { + list-style-type: none; + } +} \ 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..bf1a9ea --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/choiceTrueOrFalse.js @@ -0,0 +1,57 @@ +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', + name: gettext('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..665d957 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/element.html @@ -0,0 +1,5 @@ +
+
<%= gettext('Exercise') %>: <%= name %>
+
+
+
\ 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..9174710 --- /dev/null +++ b/src/editor/plugins/core/edumed/choice/list.js @@ -0,0 +1,98 @@ +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(), { + resetBackground: true, + 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 new file mode 100644 index 0000000..6ba5996 --- /dev/null +++ b/src/editor/plugins/core/edumed/edumed.js @@ -0,0 +1,18 @@ +define(function(require) { + +'use strict'; + +var actions = require('./actions'), + gapsActions = require('./gaps/actions'), + replaceActions = require('./replace/actions'), + orderExerciseElement = require('./order/element'), + gapsExerciseElement = require('./gaps/element'), + replaceExerciseElement = require('./replace/element'), + choiceExerciseElements = require('./choice/element'); + +return { + actions: actions.concat(gapsActions).concat(replaceActions), + 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 new file mode 100644 index 0000000..fc79e39 --- /dev/null +++ b/src/editor/plugins/core/edumed/edumed.less @@ -0,0 +1,30 @@ +@import 'order/order.less'; +@import 'gaps/gaps.less'; +@import 'replace/replace.less'; +@import 'choice/choice.less'; + +.edumed-exercise { + background-color: @grayLighter; + border: 1px lighten(#000, 35%) dashed; + margin: 15px 0; + padding: 10px; + + .header { + font-weight: bold; + font-variant: small-caps; + margin-bottom: 10px; + background-color: @grayLighter;; + } + + .btn { + line-height: 12px; + } +} + +.edumed-exercise-remove { + position: absolute; + top: 20px; + right: 5px; + padding: 0 3px; + line-height: 15px; +} \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/elementBase.js b/src/editor/plugins/core/edumed/elementBase.js new file mode 100644 index 0000000..9f87dd1 --- /dev/null +++ b/src/editor/plugins/core/edumed/elementBase.js @@ -0,0 +1,48 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + documentElement = require('modules/documentCanvas/canvas/documentElement'), + Dialog = require('views/dialog/dialog'); + + +var choiceBase = Object.create(documentElement.DocumentNodeElement.prototype); +_.extend(choiceBase, { + init: function() { + var el = this; + documentElement.DocumentNodeElement.prototype.init.call(this); + this.x = $('
    x
    '); + this.x.on('click', function() { + var dialog = Dialog.create({ + title: gettext('Removing exercise'), + text: gettext('Do you really want to remove this exercise?'), + executeButtonText: gettext('Yes'), + cancelButtonText: gettext('No, don\'t do anything!') + }); + dialog.on('execute', function(event) { + el.canvas.wlxmlDocument.transaction(function() { + el.wlxmlNode.detach(); + }, { + metadata: { + description: gettext('Removing exercise') + }, + success: function() { + event.success(); + } + }); + }); + dialog.show(); + + }); + + this.addWidget(this.x); + }, +}); + +return choiceBase; + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/gaps/actions.js b/src/editor/plugins/core/edumed/gaps/actions.js new file mode 100644 index 0000000..e91faa4 --- /dev/null +++ b/src/editor/plugins/core/edumed/gaps/actions.js @@ -0,0 +1,92 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var _ = require('libs/underscore'); + + +var createGap = { + name: 'createGap', + params: { + fragment: {type: 'context', name: 'fragment'} + }, + stateDefaults: { + icon: null, + label: gettext('Create a gap'), + execute: function(callback, params) { + var doc = params.fragment.document; + + doc.transaction(function() { + var wrapper = params.fragment.startNode.parent().wrapText({ + _with: {tagName: 'aside', attrs: {'class': 'gap'}}, + offsetStart: params.fragment.startOffset, + offsetEnd: params.fragment.endOffset, + textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()] + }), + last = _.last(wrapper.contents()); + + return doc.createFragment(doc.CaretFragment, {node: last, offset: last.getText().length}); + }, { + metadata: { + description: gettext('Create a gap') + }, + success: callback + }); + } + }, + getState: function(params) { + return { + allowed: params.fragment && + params.fragment.isValid() && + params.fragment instanceof params.fragment.TextRangeFragment && + params.fragment.hasSiblingBoundries() && + params.fragment.startNode.isInside('exercise.gap') && + !params.fragment.startNode.isInside({tagName: 'aside', klass: 'gap'}), + + description: gettext('Turn selection into a gap') + }; + } +}; + +var removeGap = { + name: 'removeGap', + params: { + fragment: {type: 'context', name: 'fragment'} + }, + stateDefaults: { + icon: null, + label: gettext('Remove a gap'), + execute: function(callback, params) { + var doc = params.fragment.document; + + doc.transaction(function() { + var ret = params.fragment.node.getParent('gap').unwrapContent(); + + return doc.createFragment(doc.CaretFragment, {node:ret.element2, offset: ret.element2.getText().length}); + }, { + metadata: { + description: gettext('Remove a gap') + }, + success: callback + }); + } + }, + getState: function(params) { + return { + allowed: params.fragment && + params.fragment.isValid() && + params.fragment instanceof params.fragment.NodeFragment && + params.fragment.node.isInside('exercise.gap') && + params.fragment.node.isInside('gap'), + + description: gettext('Remove a gap') + }; + } +}; + + +return [createGap, removeGap]; + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/gaps/element.js b/src/editor/plugins/core/edumed/gaps/element.js new file mode 100644 index 0000000..db8d0c5 --- /dev/null +++ b/src/editor/plugins/core/edumed/gaps/element.js @@ -0,0 +1,53 @@ +define(function(require) { + +'use strict'; + + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + elementBase = require('plugins/core/edumed/elementBase'), + viewTemplate = require('libs/text!./view.html'); + +var OrderExerciceElement = Object.create(elementBase); +_.extend(OrderExerciceElement, { + init: function() { + elementBase.init.call(this); + var view = $(_.template(viewTemplate)()); + this._container().append(view); + + this.createContainer(this.wlxmlNode.contents(), { + resetBackground: true, + manages: function() { + return true; + }, + dom: view.find('.content') + }); + + this.addToContextMenu('core.createGap'); + this.contextMenuActions[0].on('actionExecuted', function(ret) { + if(ret instanceof this.wlxmlNode.document.Fragment && ret.isValid()) { + this.canvas.select(ret); + } + }.bind(this)); + this.addToContextMenu('core.removeGap'); + }, + getVerticallyFirstTextElement: function() { + // doesnt container handle this? + var toret; + this.containers.some(function(container) { + toret = container.getVerticallyFirstTextElement(); + return !!toret; + }); + return toret; + } +}); + +return {tag: 'div', klass: 'exercise.gap', prototype: OrderExerciceElement}; + +}); + + + + + + diff --git a/src/editor/plugins/core/edumed/gaps/gaps.less b/src/editor/plugins/core/edumed/gaps/gaps.less new file mode 100644 index 0000000..812b0e1 --- /dev/null +++ b/src/editor/plugins/core/edumed/gaps/gaps.less @@ -0,0 +1,19 @@ +[wlxml-tag="aside"] { + [wlxml-class='gap'] { + display: inline; + } +} + +[wlxml-class="gap"] { + &:before, &:after { + color: darken(@blue, 10%); + font-weight: bold; + //vertical-align: super; + } + &:before { + content: "("; + } + &:after { + content: ")"; + } +} \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/gaps/view.html b/src/editor/plugins/core/edumed/gaps/view.html new file mode 100644 index 0000000..7d4210b --- /dev/null +++ b/src/editor/plugins/core/edumed/gaps/view.html @@ -0,0 +1,4 @@ +
    +
    <%= gettext('Exercise') %>: <%= gettext('Gaps') %>
    +
    +
    \ 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..724f2ec --- /dev/null +++ b/src/editor/plugins/core/edumed/order/element.js @@ -0,0 +1,106 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var _ = require('libs/underscore'); + +var elementBase = require('plugins/core/edumed/elementBase'), + OrderExerciseView = require('./view'); + +var OrderExerciceElement = Object.create(elementBase); +_.extend(OrderExerciceElement, { + init: function() { + elementBase.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(), { + resetBackground: true, + manages: function(node, removedFrom) { + if(node.is('list.orderable') || (removedFrom && removedFrom.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(); + } + }, + onNodeDetached: function(event) { + var node = event.meta.node; + if(node.is('item.answer') && event.meta.parent && event.meta.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..e0c1b52 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/order.less @@ -0,0 +1,70 @@ +@exercise-order-gray: darken(#ddd, 15%); +.exercise-order { + + overflow-x: hidden; + + .description { + margin-bottom: 20px; + } + + .wrapper { + display: table; + width: 100%; + border-radius: 5px; + background-color: #ddd; + } + + .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: @exercise-order-gray; + text-align: center; + border-radius: 0 5px 5px 0; + padding: 10px 10px; + + .dragEnter { + opacity: 0.5; + } + } + + .placeholder { + height: 10px; + transition: height 0.05s; + background-color: transparent; + &.active { + height: 60px; + transition: height 0.05s; + background-color: lighten(@blue,40%); + border: 1px dashed @blue; + } + width:100%; + } + + ol { + margin: 0; + counter-reset: edumed-order-counter; + } + ol li { + list-style-type: none; + cursor: move; + .dragged { + opacity: 0.6; + } + } + + .nav-tabs > .active > a { + background-color: @exercise-order-gray; + } +} \ 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..561de89 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/view.html @@ -0,0 +1,10 @@ +
    +
    <%= gettext('Exercise') %>: <%= gettext('Order') %>
    +
    + +
    +
      +
      \ 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..a90ded4 --- /dev/null +++ b/src/editor/plugins/core/edumed/order/view.js @@ -0,0 +1,214 @@ +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'); + e.preventDefault(); + }.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('.handle'); + + 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(), { + resetBackground: true, + 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..33e242a --- /dev/null +++ b/src/editor/plugins/core/edumed/order/viewItem.html @@ -0,0 +1,9 @@ +
    1. +
      +
      + +
      +
      +
      +
      +
    2. \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/replace/actions.js b/src/editor/plugins/core/edumed/replace/actions.js new file mode 100644 index 0000000..be4782a --- /dev/null +++ b/src/editor/plugins/core/edumed/replace/actions.js @@ -0,0 +1,154 @@ +define(function(require) { + +'use strict'; + +/* globals gettext */ + +var _ = require('libs/underscore'), + Dialog = require('views/dialog/dialog'); + +var markToReplace = { + name: 'markToReplace', + params: { + fragment: {type: 'context', name: 'fragment'} + }, + stateDefaults: { + icon: null, + label: gettext('Mark to replace'), + execute: function(callback, params) { + var doc = params.fragment.document, + dialog = Dialog.create({ + title: gettext('Enter text to replace with'), + executeButtonText: gettext('Apply'), + cancelButtonText: gettext('Cancel'), + fields: [ + {label: gettext('Text'), name: 'text', type: 'input'} + ] + }); + + + dialog.on('execute', function(event) { + doc.transaction(function() { + var wrapper = params.fragment.startNode.parent().wrapText({ + _with: {tagName: 'span', attrs: {'class': 'answer', answer: event.formData.text}}, + offsetStart: params.fragment.startOffset, + offsetEnd: params.fragment.endOffset, + textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()] + }), + last = _.last(wrapper.contents()); + + return doc.createFragment(doc.CaretFragment, {node: last, offset: last.getText().length}); + }, { + metadata: { + description: gettext('Mark to replace') + }, + success: function(ret) { event.success(); callback(ret);} + }); + }); + dialog.show(); + + } + }, + getState: function(params) { + return { + allowed: params.fragment && + params.fragment.isValid() && + params.fragment instanceof params.fragment.TextRangeFragment && + params.fragment.hasSameBoundries() && + params.fragment.startNode.isInside('exercise.replace') && + !params.fragment.startNode.isInside({tagName: 'span', klass: 'answer'}), + + description: gettext('Mark selection to replacement') + }; + } +}; + +var removeReplaceMark = { + name: 'removeReplaceMark', + params: { + fragment: {type: 'context', name: 'fragment'} + }, + stateDefaults: { + icon: null, + label: gettext('Remove replace mark'), + execute: function(callback, params) { + var doc = params.fragment.document; + + doc.transaction(function() { + var ret = params.fragment.node.getParent('answer').unwrapContent(); + + return doc.createFragment(doc.CaretFragment, {node:ret.element2, offset: ret.element2.getText().length}); + }, { + metadata: { + description: gettext('Remove replace mark') + }, + success: callback + }); + } + }, + getState: function(params) { + return { + allowed: params.fragment && + params.fragment.isValid() && + params.fragment instanceof params.fragment.NodeFragment && + params.fragment.node.isInside('exercise.replace') && + params.fragment.node.isInside('answer'), + + description: gettext('Remove replace mark') + }; + } +}; + +var editReplaceMark = { + name: 'editReplaceMark', + params: { + fragment: {type: 'context', name: 'fragment'} + }, + stateDefaults: { + icon: null, + label: gettext('Edit replace mark'), + execute: function(callback, params) { + var doc = params.fragment.document, + answerNode = params.fragment.node.getParent('answer'), + dialog = Dialog.create({ + title: gettext('Edit text to replace with'), + executeButtonText: gettext('Apply'), + cancelButtonText: gettext('Cancel'), + fields: [ + {label: gettext('Text'), name: 'text', type: 'input', initialValue: answerNode.getAttr('answer')} + ] + }); + + + dialog.on('execute', function(event) { + doc.transaction(function() { + answerNode.setAttr('answer', event.formData.text); + var node = answerNode.contents()[0]; + return doc.createFragment(doc.CaretFragment, {node: node, offset: node.getText().length}); + }, { + metadata: { + description: gettext('Edit answer') + }, + success: function(ret) { event.success(); callback(ret);} + }); + }); + dialog.show(); + + } + }, + getState: function(params) { + return { + allowed: params.fragment && + params.fragment.isValid() && + params.fragment instanceof params.fragment.NodeFragment && + params.fragment.node.isInside('exercise.replace') && + params.fragment.node.isInside('answer'), + + description: gettext('Mark selection to replacement') + }; + } +}; + +return [markToReplace, removeReplaceMark, editReplaceMark]; + +}); \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/replace/element.js b/src/editor/plugins/core/edumed/replace/element.js new file mode 100644 index 0000000..f4157d2 --- /dev/null +++ b/src/editor/plugins/core/edumed/replace/element.js @@ -0,0 +1,87 @@ +define(function(require) { + +'use strict'; + + +var $ = require('libs/jquery'), + _ = require('libs/underscore'), + elementBase = require('plugins/core/edumed/elementBase'), + genericElement = require('modules/documentCanvas/canvas/genericElement'), + viewTemplate = require('libs/text!./view.html'), + tipTemplate = require('libs/text!./tip.html'); + +var AnswerElement = Object.create(genericElement); +_.extend(AnswerElement, { + init: function() { + genericElement.init.call(this); + this.tip = $(tipTemplate); + this.tip.text(this.wlxmlNode.getAttr('answer') || ''); + this.tip.on('click', function(e) { + var doc = this.wlxmlNode.document, + textNode = this.wlxmlNode.contents()[0]; + e.preventDefault(); + e.stopPropagation(); + + if(textNode) { + this.canvas.select(doc.createFragment(doc.CaretFragment, {node:textNode, offset: textNode.getText().length})); + this.canvas.showContextMenu(this, {x: e.clientX, y: e.clientY}); + } + }.bind(this)); + this.addWidget(this.tip); + }, + onNodeAttrChange: function(event) { + if(event.meta.attr === 'answer') { + this.tip.text(event.meta.newVal || ''); + } + } +}); + +var ReplaceExerciseElement = Object.create(elementBase); +_.extend(ReplaceExerciseElement, { + init: function() { + elementBase.init.call(this); + var view = $(_.template(viewTemplate)()); + this._container().append(view); + + + this.elementsRegister.register( + {tag: 'span', klass: 'answer', prototype: AnswerElement} + ); + + this.createContainer(this.wlxmlNode.contents(), { + resetBackground: true, + manages: function() { + return true; + }, + dom: view.find('.content') + }); + + this.addToContextMenu('core.markToReplace'); + this.contextMenuActions[0].on('actionExecuted', function(ret) { + if(ret instanceof this.wlxmlNode.document.Fragment && ret.isValid()) { + this.canvas.select(ret); + } + }.bind(this)); + this.addToContextMenu('core.editReplaceMark'); + this.addToContextMenu('core.removeReplaceMark'); + + }, + getVerticallyFirstTextElement: function() { + var toret; + this.containers.some(function(container) { + toret = container.getVerticallyFirstTextElement(); + return !!toret; + }); + return toret; + } +}); + +return {tag: 'div', klass: 'exercise.replace', prototype: ReplaceExerciseElement}; + +}); + + + + + + diff --git a/src/editor/plugins/core/edumed/replace/replace.less b/src/editor/plugins/core/edumed/replace/replace.less new file mode 100644 index 0000000..161a538 --- /dev/null +++ b/src/editor/plugins/core/edumed/replace/replace.less @@ -0,0 +1,16 @@ +.exercise-replace { + [wlxml-class="answer"] { + //color: @red; + text-decoration: line-through; + } + + .tip { + position: absolute; + top: -12px; + font-size: 9px; + line-height: 12px; + color: lighten(@red, 10%); + white-space: nowrap; + } + +} \ No newline at end of file diff --git a/src/editor/plugins/core/edumed/replace/tip.html b/src/editor/plugins/core/edumed/replace/tip.html new file mode 100644 index 0000000..5a82bef --- /dev/null +++ b/src/editor/plugins/core/edumed/replace/tip.html @@ -0,0 +1,2 @@ +
      + diff --git a/src/editor/plugins/core/edumed/replace/view.html b/src/editor/plugins/core/edumed/replace/view.html new file mode 100644 index 0000000..d427539 --- /dev/null +++ b/src/editor/plugins/core/edumed/replace/view.html @@ -0,0 +1,4 @@ +
      +
      <%= gettext('Exercise') %>: <%= gettext('Replace') %>
      +
      +
      \ No newline at end of file diff --git a/src/editor/plugins/core/lists.js b/src/editor/plugins/core/lists.js index 7a9d321..65b6e69 100644 --- a/src/editor/plugins/core/lists.js +++ b/src/editor/plugins/core/lists.js @@ -8,10 +8,14 @@ var getBoundriesForAList = function(fragment) { var node; if(fragment instanceof fragment.RangeFragment && fragment.hasSiblingBoundries()) { - return fragment.boundriesSiblingParents(); + return fragment.startNode.hasSameContextRoot(fragment.endNode) && fragment.boundriesSiblingParents(); } if(fragment instanceof fragment.NodeFragment) { node = fragment.node.getNearestElementNode(); + if(node.isContextRoot()) { + node = fragment.node; + } + return { node1: node, node2: node @@ -152,7 +156,7 @@ var toggleListAction = function(type) { } var boundries = getBoundriesForAList(params.fragment); - if(boundries) { + if(boundries && boundries.node1.hasSameContextRoot(boundries.node2)) { return { allowed: true, description: interpolate(gettext('Make %s fragment(s) into list'), [countItems(getBoundriesForAList(params.fragment))]), diff --git a/src/editor/styles/main.less b/src/editor/styles/main.less index e1e08a3..6322459 100644 --- a/src/editor/styles/main.less +++ b/src/editor/styles/main.less @@ -5,6 +5,7 @@ @import '../plugins/plugins.less'; @import '../views/openSelect/openSelect.less'; @import '../views/dialog/dialog.less'; +@import '../views/menu/menu.less'; @import '../modules/rng/rng.less'; @import '../modules/documentCanvas/documentCanvas.less'; @import '../modules/sourceEditor/sourceEditor.less'; diff --git a/src/editor/views/menu/menu.html b/src/editor/views/menu/menu.html new file mode 100644 index 0000000..bfb61fe --- /dev/null +++ b/src/editor/views/menu/menu.html @@ -0,0 +1,7 @@ +
      + +
      + diff --git a/src/editor/views/menu/menu.js b/src/editor/views/menu/menu.js new file mode 100644 index 0000000..dd8d01a --- /dev/null +++ b/src/editor/views/menu/menu.js @@ -0,0 +1,66 @@ +define(function(require) { + +'use strict'; + +var $ = require('libs/jquery'), + Backbone = require('libs/backbone'), + template = require('libs/text!./menu.html'), + itemTemplate = require('libs/text!./menuitem.html'); + + +var Menu = function() { + this.dom = $(template); + this.actions = []; +}; +$.extend(Menu.prototype, { + addAction: function(action) { + var item = new MenuItem(action); + item.on('execute', function() { + this.close(); + action.execute(); + }.bind(this)); + + this.actions.push(action); + this.dom.find('ul').append(item.dom); + }, + close: function() { + this.dom.remove(); + }, + show: function() { + this.dom.find('.dropdown-menu').dropdown('toggle'); + }, + updateContextParam: function(k, v) { + this.actions.forEach(function(action) { + action.updateContextParam(k, v); + }); + } +}); + +var MenuItem = function(action) { + this.action = action; + this.dom = $(itemTemplate); + + action.on('paramsChanged', function() { + this.render(); + }.bind(this)); + + this.dom.on('click', function() { + if(this.action.getState().allowed) { + this.trigger('execute'); + } + }.bind(this)); + + this.render(); +}; +$.extend(MenuItem.prototype, Backbone.Events, { + render: function() { + var state = this.action.getState(); + this.dom.find('a').text(state.label || '?'); + this.dom.toggleClass('disabled', !state.allowed); + } +}); + + +return Menu; + +}); diff --git a/src/editor/views/menu/menu.less b/src/editor/views/menu/menu.less new file mode 100644 index 0000000..ca9300d --- /dev/null +++ b/src/editor/views/menu/menu.less @@ -0,0 +1,3 @@ +.rng-menu { + position: absolute; +} \ No newline at end of file diff --git a/src/editor/views/menu/menuitem.html b/src/editor/views/menu/menuitem.html new file mode 100644 index 0000000..a1085b9 --- /dev/null +++ b/src/editor/views/menu/menuitem.html @@ -0,0 +1 @@ +
    3. \ No newline at end of file diff --git a/src/fnpjs/runner.js b/src/fnpjs/runner.js index f3edd31..0dd52e5 100644 --- a/src/fnpjs/runner.js +++ b/src/fnpjs/runner.js @@ -15,7 +15,8 @@ var Runner = function(app, modules) { plugins = [], actionDefinitions = {}, config, - actionsAppObject; + actionsAppObject, + currentContextMenu; _.each(_.keys(modules || {}), function(moduleName) { if(_.contains(app.permissions[moduleName] || [], 'handleEvents')) { @@ -78,9 +79,18 @@ var Runner = function(app, modules) { this.registerActionsAppObject = function(_actionsAppObject) { actionsAppObject = _actionsAppObject; }; + + this.showContextMenu = function(menu, coors) { + if(currentContextMenu) { + currentContextMenu.close(); + } + currentContextMenu = menu; + $(config.rootSelector).append(menu.dom); + menu.dom.css({top: coors.y, left: coors.x}); + menu.show(); + }; }; - - + this.setBootstrappedData = function(moduleName, data) { bootstrappedData[moduleName] = data; }; @@ -120,6 +130,13 @@ var Runner = function(app, modules) { app.initModules.forEach(function(moduleName) { getModuleInstance(moduleName).start(); }); + + $(config.rootSelector)[0].addEventListener('click', function(e) { + if(currentContextMenu && !currentContextMenu.dom[0].contains(e.target)) { + currentContextMenu.close(); + currentContextMenu = null; + } + }, true); }; }; diff --git a/src/smartxml/core.js b/src/smartxml/core.js index 6329491..7326507 100644 --- a/src/smartxml/core.js +++ b/src/smartxml/core.js @@ -169,7 +169,11 @@ var elementNodeTransformations = { setAttr: function(name, value, silent) { var oldVal = this.getAttr(name); - this._$.attr(name, value); + if(_.isUndefined(value)) { + this._$.removeAttr(name); + } else { + this._$.attr(name, value); + } if(!silent) { this.triggerChangeEvent('nodeAttrChange', {attr: name, oldVal: oldVal, newVal: value}); } @@ -359,7 +363,7 @@ var textNodeTransformations = { newElement.prepend({text: suffix}); } - return {first: parentElement, second: newElement}; + return {first: parentElement, second: newElement, created: newElement}; }, divideWithElementNode: function(node, params) { diff --git a/src/smartxml/fragments.js b/src/smartxml/fragments.js index b1befdf..d932592 100644 --- a/src/smartxml/fragments.js +++ b/src/smartxml/fragments.js @@ -79,6 +79,9 @@ $.extend(RangeFragment.prototype, { hasSiblingBoundries: function() { return this.isValid() && this.startNode.isSiblingOf(this.endNode); }, + hasSameBoundries: function() { + return this.isValid() && this.startNode.sameNode(this.endNode); + }, boundriesSiblingParents: function() { return this.startNode.document.getSiblingParents({ node1: this.startNode, diff --git a/src/wlxml/extensions/edumed/choice.xml b/src/wlxml/extensions/edumed/choice.xml new file mode 100644 index 0000000..78d372b --- /dev/null +++ b/src/wlxml/extensions/edumed/choice.xml @@ -0,0 +1,6 @@ +
      +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/choiceSingle.xml b/src/wlxml/extensions/edumed/choiceSingle.xml new file mode 100644 index 0000000..452d4a4 --- /dev/null +++ b/src/wlxml/extensions/edumed/choiceSingle.xml @@ -0,0 +1,6 @@ +
      +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/choiceTrueOrFalse.xml b/src/wlxml/extensions/edumed/choiceTrueOrFalse.xml new file mode 100644 index 0000000..a67623c --- /dev/null +++ b/src/wlxml/extensions/edumed/choiceTrueOrFalse.xml @@ -0,0 +1,6 @@ +
      +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/edumed.js b/src/wlxml/extensions/edumed/edumed.js new file mode 100644 index 0000000..ca29ad3 --- /dev/null +++ b/src/wlxml/extensions/edumed/edumed.js @@ -0,0 +1,234 @@ +define(function(require) { + +'use strict'; + +var _ = require('libs/underscore'), + templates = { + order: require('libs/text!./order.xml'), + choice: require('libs/text!./choice.xml'), + 'choice.single': require('libs/text!./choiceSingle.xml'), + 'choice.true-or-false': require('libs/text!./choiceTrueOrFalse.xml'), + gap: require('libs/text!./gaps.xml'), + replace: require('libs/text!./replace.xml') + }; + +var Item = function(node, exerciseNode) { + Object.defineProperty(this, 'text', { + get: function() { + /* globals Node */ + var firstNode = node.contents()[0]; + if(firstNode && firstNode.nodeType === Node.TEXT_NODE) { + return firstNode.getText(); + } + return ''; + } + }); + this.node = node; + this.exerciseNode = exerciseNode; +}; +_.extend(Item.prototype, { + setText: function(text) { + /* globals Node */ + var contents = this.node.contents(); + if(contents.length === 1 && contents[0].nodeType === Node.TEXT_NODE) { + contents[0].setText(text); + } else { + contents.forEach(function(childNode) { + childNode.detach(); + }); + contents.append({text: text}); + } + }, + remove: function() { + this.node.detach(); + }, + getAnswer: function() { + var toret = parseInt(this.node.getAttr('answer'), 10); + if(_.isNaN(toret)) { + toret = 1; + } + return toret; + }, + setAnswer: function(answer) { + answer = parseInt(answer, 10); + var prev = answer; + if(!_.isNumber(answer)) { + return; + } + + this.exerciseNode.object.getItems() + .sort(function(item1, item2) { + if(item1.getAnswer() > item2.getAnswer()) { + return 1; + } + return -1; + }) + .some(function(item) { + if(item.getAnswer() === prev && !item.node.sameNode(this.node)) { + item.node.setAttr('answer', prev+1); + prev = prev + 1; + } + }.bind(this)); + this.node.setAttr('answer', answer); + + } +}); + +var isItemsList = function(node) { + return node.is('list.orderable'); +}; + + +var extension = {wlxmlClass: {'exercise.order': { + methods: { + isContextRoot: function(node) { + return this.object.isItemNode(node) || this.sameNode(node); + }, + getItems: function() { + var toret = [], + exerciseNode = this; + + this.contents().some(function(node) { + if(isItemsList(node)) { + node.contents() + .filter(function(node) { + return node.is('item.answer'); + }) + .forEach(function(node) { + toret.push(new Item(node, exerciseNode)); + }); + return true; + } + }); + return toret; + }, + isItemNode: function(node) { + var list; + if(!node) { + return; + } + this.contents().some(function(node) { + if(isItemsList(node)) { + list = node; + return true; + } + }); + return list && list.sameNode(node.parent()); + }, + getDescription: function() { + var toret = []; + this.contents().some(function(node) { + if(isItemsList(node)) { + return true; + } + toret.push(node); + }); + return toret; + } + }, + transformations: { + addItem: function(text) { + var toret; + this.contents().some(function(node) { + if(isItemsList(node)) { + var itemNode = this.document.createDocumentNode({tagName: 'div', attrs: {'class': 'item.answer', answer: this.object.getItems().length+1}}); + toret = itemNode.append({text: text}); + node.append(itemNode); + return true; + } + }.bind(this)); + return toret; + }, + setDescription: function(text) { + this.contents().some(function(node) { + var textNode; + if(node.is('p')) { + textNode = node.contents()[0]; + if(!(textNode && textNode.nodeType === Node.TEXT_NODE)) { + node.prepend({text:text}); + } else { + textNode.setText(text); + } + return true; + } + }); + } + } +}}}; + +var choiceMethods = { + isContextRoot: function(node) { + return this.object.isChoiceList(node.parent()) || this.sameNode(node); + }, + 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.wlxmlClass['exercise.gap'] = extension.wlxmlClass['exercise.replace'] = { + methods: { + isContextRoot: function(node) { + return this.sameNode(node); + } + } +}; + +extension.document = { + methods: { + edumedCreateExerciseNode: function(klass) { + void(klass); + return this.createDocumentNode(templates[klass]); + } + } +}; + +return extension; + +}); \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/edumed.test.js b/src/wlxml/extensions/edumed/edumed.test.js new file mode 100644 index 0000000..c039f0d --- /dev/null +++ b/src/wlxml/extensions/edumed/edumed.test.js @@ -0,0 +1,70 @@ +define(function(require) { + +'use strict'; +/* globals describe, it */ + +var chai = require('libs/chai'), + wlxml = require('wlxml/wlxml'); + //edumedExtension = require('./edumed.js'); + +var expect = chai.expect; + + +var getDocumentFromXML = function(xml, options) { + var doc = wlxml.WLXMLDocumentFromXML(xml, options || {}); + //doc.registerExtension(edumedExtension); + return doc; +}; + + +describe('Setting answer', function() { + it('sets answer (1)', function() { + /* jshint multistr:true */ + var doc = getDocumentFromXML('\ +
      \ +
      \ +
      Element 3
      \ +
      Element 1
      \ +
      Element 2
      \ +
      \ +
      '); + + doc.root.object.getItems()[2].setAnswer(1); + + var items = doc.root.object.getItems(); + + expect(items[0].getAnswer()).to.equal(3); + expect(items[1].getAnswer()).to.equal(2); + expect(items[2].getAnswer()).to.equal(1); + + }); + it('sets answer (2)', function() { + /* jshint multistr:true */ + var doc = getDocumentFromXML('\ +
      \ +
      \ +
      Element 1
      \ +
      Element 2
      \ +
      Element 3
      \ +
      \ +
      '); + doc.transaction(function() { + doc.root.object.getItems()[2].setAnswer(2); + }, { + error: function(e) { throw e;} + }); + + + var items = doc.root.object.getItems(); + + expect(items[0].getAnswer()).to.equal(1); + expect(items[1].getAnswer()).to.equal(3); + expect(items[2].getAnswer()).to.equal(2); + + }); +}); + + + + +}); \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/gaps.xml b/src/wlxml/extensions/edumed/gaps.xml new file mode 100644 index 0000000..0a5b40f --- /dev/null +++ b/src/wlxml/extensions/edumed/gaps.xml @@ -0,0 +1,3 @@ +
      +
      +
      \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/order.xml b/src/wlxml/extensions/edumed/order.xml new file mode 100644 index 0000000..167e981 --- /dev/null +++ b/src/wlxml/extensions/edumed/order.xml @@ -0,0 +1,5 @@ +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/src/wlxml/extensions/edumed/replace.xml b/src/wlxml/extensions/edumed/replace.xml new file mode 100644 index 0000000..2ff71c2 --- /dev/null +++ b/src/wlxml/extensions/edumed/replace.xml @@ -0,0 +1,3 @@ +
      +
      +
      \ No newline at end of file diff --git a/src/wlxml/extensions/list/list.js b/src/wlxml/extensions/list/list.js index 3ee65d4..8d88115 100644 --- a/src/wlxml/extensions/list/list.js +++ b/src/wlxml/extensions/list/list.js @@ -111,7 +111,7 @@ extension.document.transformations.extractItems = { extractedItems = [], succeedingItems = [], items = list.contents(), - listIsNested = list.parent().is('item'); + listIsNested = list.parent().is('item') && !list.parent().isContextRoot(); items.forEach(function(item, idx) { @@ -133,7 +133,7 @@ extension.document.transformations.extractItems = { reference.after(item); reference = item; if(!listIsNested) { - item.setClass(''); + item.setClass('p'); } }); if(precedingItems.length === 0) { @@ -143,14 +143,14 @@ extension.document.transformations.extractItems = { extractedItems.forEach(function(item) { reference.before(item); if(!listIsNested) { - item.setClass(''); + item.setClass('p'); } }); } else { extractedItems.forEach(function(item) { reference.after(item); if(!listIsNested) { - item.setClass(''); + item.setClass('p'); } reference = item; }); diff --git a/src/wlxml/extensions/list/list.test.js b/src/wlxml/extensions/list/list.test.js index 2d47778..277e828 100644 --- a/src/wlxml/extensions/list/list.test.js +++ b/src/wlxml/extensions/list/list.test.js @@ -136,10 +136,10 @@ describe('Lists extension', function() { expect(list1.contents()[0].contents()[0].getText()).to.equal('0', 'first item of the first list is a first item of the original list'); expect(oldItem1.contents()[0].getText()).to.equal('1', 'first item got extracted'); - expect(oldItem1.getClass() === '').to.equal(true, 'first extracted element has no wlxml class'); + expect(oldItem1.getClass() === 'p').to.equal(true, 'first extracted element became a paragraph'); expect(oldItem2.contents()[0].getText()).to.equal('2', 'second item got extracted'); - expect(oldItem2.getClass() === '').to.equal(true, 'second extracted element has no wlxml class'); + expect(oldItem2.getClass() === 'p').to.equal(true, 'second extracted element became a paragraph'); expect(list2.is('list')).to.equal(true, 'last section child is a list'); expect(list2.contents().length).to.equal(1, 'second list has one child'); @@ -414,7 +414,7 @@ describe('Lists extension', function() { expect(sectionContents[0].is('list')).to.equal(true, 'first child is a list'); expect(extractedItem.getTagName()).to.equal('div', 'extracted item is a wlxml div'); - expect(extractedItem.getClass()).to.equal('', 'extracted item has no wlxml class'); + expect(extractedItem.getClass()).to.equal('p', 'extracted item became a paragraph'); expect(extractedItem.contents()[0].getText()).to.equal('1.1', 'extracted item ok'); expect(sectionContents[2].is('list')).to.equal(true, 'second child is a list'); }); diff --git a/src/wlxml/wlxml.js b/src/wlxml/wlxml.js index 5925ee6..ef70d3d 100644 --- a/src/wlxml/wlxml.js +++ b/src/wlxml/wlxml.js @@ -4,8 +4,9 @@ define([ 'smartxml/smartxml', 'smartxml/transformations', 'wlxml/extensions/metadata/metadata', - 'wlxml/extensions/comments/comments' -], function($, _, smartxml, transformations, metadataExtension, commentExtension) { + 'wlxml/extensions/comments/comments', + 'wlxml/extensions/edumed/edumed' +], function($, _, smartxml, transformations, metadataExtension, commentExtension, edumedExtension) { 'use strict'; @@ -24,11 +25,39 @@ var WLXMLDocumentNodeMethods = { me.concat(this.parents()).some(function(node) { if(node.is(query)) { toret = node; + } + return !!toret || (!node.sameNode(this) && node.isContextRoot()); + }.bind(this)); + + return toret; + }, + isContextRoot: function() { + var me = this.nodeType === Node.ELEMENT_NODE ? [this] : [], + toret = false; + if(!this.parent()) { + return true; + } + me.concat(this.parents()).some(function(node) { + if(_.isFunction(node.object.isContextRoot) && node.object.isContextRoot(this)) { + toret = true; return true; } - }); + }.bind(this)); return toret; }, + getContextRoot: function() { + var contextRoot; + [this].concat(this.parents()).some(function(n) { + if(n.isContextRoot()) { + contextRoot = n; + return true; + } + }); + return contextRoot; + }, + hasSameContextRoot: function(other) { + return this.getContextRoot().sameNode(other.getContextRoot()); + } }; var getClassLists = function(klassName) { @@ -90,6 +119,16 @@ $.extend(WLXMLElementNode.prototype, WLXMLDocumentNodeMethods, smartxml.ElementN return child.is(query); }.bind(this)); }, + find: function(query) { + var doc = this.document; + return this._$.find('*') + .map(function() { + return doc.createDocumentNode(this); + }) + .filter(function() { + return this.is(query); + }); + }, _getXMLDOMToDump: function() { var DOM = this._$.clone(true, true), @@ -157,7 +196,6 @@ var WLXMLDocumentNode = function() { }; WLXMLDocumentNode.prototype = Object.create(smartxml.DocumentNode.prototype); - var WLXMLTextNode = function() { smartxml.TextNode.apply(this, arguments); }; @@ -169,7 +207,7 @@ $.extend(WLXMLTextNode.prototype, WLXMLDocumentNodeMethods, { var WLXMLDocument = function(xml, options) { this.classMethods = {}; this.classTransformations = {}; - smartxml.Document.call(this, xml, [metadataExtension, commentExtension]); + smartxml.Document.call(this, xml, [metadataExtension, commentExtension, edumedExtension]); this.options = options; }; diff --git a/src/wlxml/wlxml.test.js b/src/wlxml/wlxml.test.js index 2b495a8..93b6df9 100644 --- a/src/wlxml/wlxml.test.js +++ b/src/wlxml/wlxml.test.js @@ -239,6 +239,36 @@ describe('WLXMLDocument', function() { expect(testClassNode.object.testTransformation2().sameNode(testClassNode)).to.equal(true, '2'); }); }); + + describe('Context roots', function() { + var doc = getDocumentFromXML('
      '); + doc.registerExtension({wlxmlClass: {a: {methods: { + isContextRoot: function(node) { + return node.getClass() === 'b'; + } + }}}}); + + var divA = doc.root.contents()[0], + divB = divA.contents()[0], + divC = divB.contents()[0]; + + it('allows extensions declaring a node as a context root', function() { + expect(divC.isContextRoot()).to.equal(false, 'c is not a context root'); + expect(divB.isContextRoot()).to.equal(true, 'b is a context root'); + expect(divA.isContextRoot()).to.equal(false, 'a is not a context root'); + }); + + it('closes context for parent context quering methods', function() { + expect(divC.isInside('b')).to.equal(true, 'c inside b'); + expect(divC.isInside('a')).to.equal(false, 'c not inside a'); + expect(divC.isInside({tagName: 'section'})).to.equal(false, 'c not inside section'); + + expect(divB.isInside('a')).to.equal(true, 'b inside a'); + expect(divB.isInside({tagName: 'section'})).to.equal(true, 'b inside section'); + + expect(divA.isInside({tagName: 'section'})).to.equal(true, 'a inside section'); + }); + }); }); }); \ No newline at end of file