{pattern: 'libs/*.js', included: false},
{pattern: 'src/**/*.js', included: false},
{pattern: 'src/**/*.html', included: false},
+ {pattern: 'src/**/*.xml', included: false},
'tests/main.js',
];
'libs/underscore',
'libs/backbone',
'fnpjs/logging/logging',
+'views/menu/menu',
'modules/documentCanvas/canvas/documentElement',
'modules/documentCanvas/canvas/keyboard',
'modules/documentCanvas/canvas/utils',
'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 */
});
-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},
$.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;
},
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) {
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()) {
}
},
+ createAction: function(fqName, config) {
+ return this.sandbox.createAction(fqName, config);
+ },
+
setupEventHandling: function() {
var canvas = this;
}
});
+ 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();
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});
+ }
}
});
});
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);
}
};
var Container = function(nodes, params, element) {
+ params = params || {};
_.extend(this, params);
this.dom = this.dom || $('<div>');
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);
}
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);
--- /dev/null
+.canvas-container {
+ .reset-background {
+ background-color: @white;
+ }
+}
var DocumentNodeElement = function(wlxmlNode, canvas) {
DocumentElement.call(this, wlxmlNode, canvas);
this.containers = [];
+ this.elementsRegister = canvas.createElementsRegister();
+ this.contextMenuActions = [];
this.init(this.dom);
};
if(params instanceof DocumentElement) {
element = params;
} else {
- element = e.canvas.createElement(params);
+ element = e.createElement(params);
}
if(element.dom) {
e.dom[action](element.dom);
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;
// })
this.dom.css('display', what);
this._container().css('display', what);
+ },
+ children: function() {
+ return [];
}
});
if(params instanceof DocumentNodeElement) {
element = params;
} else {
- element = this.canvas.createElement(params);
+ element = this.parent().createElement(params);
}
if(element.dom) {
this.dom.wrap('<div>');
if(params instanceof DocumentNodeElement) {
element = params;
} else {
- element = this.canvas.createElement(params);
+ element = this.createElement(params);
}
if(element.dom) {
this.dom.wrap('<div>');
}
}.bind(this));
}
- if(!Factory) {
- Factory = this.BaseType;
- }
return Factory;
}
});
},
refresh: function() {
- if(this.wlxmlNode.getTagName() === 'span') {
+ if(this.wlxmlNode.getTagName() === 'span' || this.wlxmlNode.getTagName() === 'aside') {
if(this.containsBlock()) {
this.displayAsBlock();
} else {
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() {
}
},
+ {
+ 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});
}, {
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;
});
}
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
});
}
},
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);
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);
}
}
var canvas = canvas3.fromXMLDocument(null, canvasElements, {
user: sandbox.getConfig().user
- });
+ }, sandbox);
var canvasWrapper = $(template);
var shownAlready = false;
var scrollbarPosition = 0,
@import 'nodes.less';
@import 'canvas/canvas.less';
+@import 'canvas/container.less';
@import 'canvas/documentElement.less';
@import 'canvas/genericElement.less';
@import 'canvas/gutter.less';
[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
.toolbar-widget {
display: inline-block;
- margin: 0 5px;
+ margin: 0;
div {
display: inline-block;
}
+ .btn {
+ border-radius: 0;
+ }
}
}
+@import 'edumed/edumed.less';
+
.canvas-widget-footnote-handle {
display: inline;
outline: 0px solid transparent;
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: {
return true; // break
}
});
+
+ /* <hack>
+ /*
+ 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);
+ /* </hack> */
+
parentDescribingNodes.forEach(function(node) {
newNodes.first.append(node);
});
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')) {
} 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;
}
}
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) {
});
};
-plugin.canvasElements = canvasElements;
+plugin.canvasElements = canvasElements.concat(edumed.canvasElements);
return plugin;
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('<section>Alice <span>h</span> a cat</section>'),
k = new Keyboard(c);
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('<section><span>a</span><span>b</span></section>'),
+ 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() {
expect(selection.offset).to.equal(0);
});
+ it('does nothing on an empty paragraph', function() {
+ var c = getCanvasFromXML('<section><div class="p">a</div></section>'),
+ 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('<section><div class="p">this <span>is</span> a paragraph</div></section>'),
k = new Keyboard(c);
});
});
+ 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('<section><div class="list"><div class="item">item</div></div></section>'),
+ 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('<section><div class="p">a</div><div class="p">b</div></section>'),
--- /dev/null
+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')})
+];
+
+});
--- /dev/null
+@import 'choiceTrueOrFalse.less';
+@import 'choiceSingle.less';
+@import 'choiceMulti.less';
\ No newline at end of file
--- /dev/null
+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;
+
+});
+
+
--- /dev/null
+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 = $('<input type="checkbox">')
+ .attr('checked', checked);
+ this.dom.on('click', function() {
+ onValueChange(this.checked);
+ });
+};
+
+return choiceMulti;
+
+});
\ No newline at end of file
--- /dev/null
+.exercise-multi {
+ input {
+ margin-right: 15px;
+ }
+ ul {
+ margin: 0;
+ }
+ li {
+ list-style-type: none;
+ }
+}
\ No newline at end of file
--- /dev/null
+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 = $('<input type="radio">')
+ .attr('checked', checked)
+ .attr('name', name);
+ this.dom.on('change', function() {
+ onValueChange(this.checked);
+ });
+};
+
+return choiceSingle;
+
+});
\ No newline at end of file
--- /dev/null
+.exercise-single {
+ input {
+ margin-right: 15px;
+ }
+ ul {
+ margin: 0;
+ }
+ li {
+ list-style-type: none;
+ }
+}
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+.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
--- /dev/null
+<div class="edumed-exercise exercise-<%= type %>">
+ <div class="header"><%= gettext('Exercise') %>: <%= name %></div>
+ <div class="description"></div>
+ <div class="list"></div>
+</div>
\ No newline at end of file
--- /dev/null
+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},
+];
+
+});
--- /dev/null
+<div class="exercise-list">
+ <ul></ul>
+</div>
\ No newline at end of file
--- /dev/null
+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;
+
+});
--- /dev/null
+<li><span class="prefix"></span><span class="content"></span></li>
--- /dev/null
+<div class="switch">
+ <span class="true"><%= gettext('true') %></span><span class="false"><%= gettext('false') %></span>
+</div>
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+@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
--- /dev/null
+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 = $('<div class="edumed-exercise-remove btn btn-mini btn-danger">x</div>');
+ 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
--- /dev/null
+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
--- /dev/null
+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};
+
+});
+
+
+
+
+
+
--- /dev/null
+[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
--- /dev/null
+<div class="edumed-exercise exercise-gaps">
+ <div class="header"><%= gettext('Exercise') %>: <%= gettext('Gaps') %></div>
+ <div class="content"></div>
+</div>
\ No newline at end of file
--- /dev/null
+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};
+
+});
--- /dev/null
+@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
--- /dev/null
+<div class="edumed-exercise exercise-order">
+ <div class="header"><%= gettext('Exercise') %>: <%= gettext('Order') %></div>
+ <div class="description"></div>
+ <ul class="nav nav-tabs modePills">
+ <li class="active" mode="initial"><a href="#"><%= gettext('Initial') %></a></li>
+ <li mode="correct"><a href="#"><%= gettext('Solution') %></a></li>
+ </ul>
+ <div class="placeholder placeholder-top"></div>
+ <ol></ol>
+</div>
\ No newline at end of file
--- /dev/null
+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;
+
+});
+
+
--- /dev/null
+<li>
+ <div class="wrapper">
+ <div class="handle" draggable="true">
+ <i class="icon-resize-vertical icon-white"></i>
+ </div>
+ <div class="content"></div>
+ </div>
+ <div class="placeholder"></div>
+</li>
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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};
+
+});
+
+
+
+
+
+
--- /dev/null
+.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
--- /dev/null
+<div class="tip"></div>
+
--- /dev/null
+<div class="edumed-exercise exercise-replace">
+ <div class="header"><%= gettext('Exercise') %>: <%= gettext('Replace') %></div>
+ <div class="content"></div>
+</div>
\ No newline at end of file
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
}
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))]),
@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';
--- /dev/null
+<div class="rng-menu">
+ <div class="dropdown">
+ <ul class="dropdown-menu">
+ </ul>
+ </div>
+</div>
+
--- /dev/null
+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;
+
+});
--- /dev/null
+.rng-menu {
+ position: absolute;
+}
\ No newline at end of file
--- /dev/null
+<li><a href="#"></li>
\ No newline at end of file
plugins = [],
actionDefinitions = {},
config,
- actionsAppObject;
+ actionsAppObject,
+ currentContextMenu;
_.each(_.keys(modules || {}), function(moduleName) {
if(_.contains(app.permissions[moduleName] || [], 'handleEvents')) {
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;
};
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);
};
};
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});
}
newElement.prepend({text: suffix});
}
- return {first: parentElement, second: newElement};
+ return {first: parentElement, second: newElement, created: newElement};
},
divideWithElementNode: function(node, params) {
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,
--- /dev/null
+<div class="exercise.choice">
+ <div class="p"></div>
+ <div class="list">
+ <div class="item.answer" answer="true"></div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<div class="exercise.choice.single">
+ <div class="p"></div>
+ <div class="list">
+ <div class="item.answer" answer="true"></div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<div class="exercise.choice.true-or-false">
+ <div class="p"></div>
+ <div class="list">
+ <div class="item.answer" answer="true"></div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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('\
+ <div class="exercise.order">\
+ <div class="list.orderable">\
+ <div class="item.answer" answer="3">Element 3</div>\
+ <div class="item.answer" answer="1">Element 1</div>\
+ <div class="item.answer" answer="2">Element 2</div>\
+ </div>\
+ </div>');
+
+ 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('\
+ <div class="exercise.order">\
+ <div class="list.orderable">\
+ <div class="item.answer" answer="1">Element 1</div>\
+ <div class="item.answer" answer="2">Element 2</div>\
+ <div class="item.answer" answer="3">Element 3</div>\
+ </div>\
+ </div>');
+ 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
--- /dev/null
+<div class="exercise.gap">
+ <div class="p"></div>
+</div>
\ No newline at end of file
--- /dev/null
+<div class="exercise.order">
+ <div class="p"></div>
+ <div class="list.orderable">
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<div class="exercise.replace">
+ <div class="p"></div>
+</div>
\ No newline at end of file
extractedItems = [],
succeedingItems = [],
items = list.contents(),
- listIsNested = list.parent().is('item');
+ listIsNested = list.parent().is('item') && !list.parent().isContextRoot();
items.forEach(function(item, idx) {
reference.after(item);
reference = item;
if(!listIsNested) {
- item.setClass('');
+ item.setClass('p');
}
});
if(precedingItems.length === 0) {
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;
});
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');
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');
});
'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';
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) {
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),
};
WLXMLDocumentNode.prototype = Object.create(smartxml.DocumentNode.prototype);
-
var WLXMLTextNode = function() {
smartxml.TextNode.apply(this, arguments);
};
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;
};
expect(testClassNode.object.testTransformation2().sameNode(testClassNode)).to.equal(true, '2');
});
});
+
+ describe('Context roots', function() {
+ var doc = getDocumentFromXML('<section><div class="a"><div class="b"><div class="c"></div></div></div></section>');
+ 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