1 define(function(require) {
6 var _ = require('libs/underscore'),
7 templates = require('plugins/core/templates'),
8 footnote = require('plugins/core/footnote'),
9 switchTo = require('plugins/core/switch'),
10 lists = require('plugins/core/lists'),
11 plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}, documentNode: {}}},
12 Dialog = require('views/dialog/dialog'),
13 canvasElements = require('plugins/core/canvasElements'),
14 metadataEditor = require('plugins/core/metadataEditor/metadataEditor'),
15 edumed = require('plugins/core/edumed/edumed'),
16 attachments = require('views/attachments/attachments');
21 var exerciseFix = function(newNodes) {
22 var list, exercise, max, addedItem, answerValues;
23 if(newNodes.created.is('item')) {
24 list = newNodes.created.parent();
25 exercise = list.parent();
26 if(exercise && exercise.is('exercise')) {
27 if(exercise.is('exercise.order')) {
28 answerValues = exercise.object.getItems()
30 if(!addedItem && item.node.sameNode(newNodes.created)) {
33 return item.getAnswer();
35 max = Math.max.apply(Math.max, answerValues);
36 addedItem.setAnswer(max + 1);
42 plugin.documentExtension.textNode.transformations = {
44 impl: function(args) {
46 isSpan = node.parent().getTagName() === 'span',
47 parentDescribingNodes = [],
49 newNodes = node.split({offset: args.offset});
50 newNodes.second.contents()
51 .filter(function(child) {
52 return child.object.describesParent;
54 .forEach(function(child) {
56 parentDescribingNodes.push(child);
59 [newNodes.first, newNodes.second].some(function(newNode) {
60 if(!(newNode.contents().length)) {
61 emptyText = newNode.append({text: ''});
68 This makes sure that adding a new item to the list in some of the edumed exercises
69 sets an answer attribute that makes sense (and not just copies it which would create
72 This won't be neccessary when/if we introduce canvas element own key event handlers.
74 Alternatively, WLXML elements could implement their own item split methods that we
77 exerciseFix(newNodes);
80 parentDescribingNodes.forEach(function(node) {
81 newNodes.first.append(node);
86 var copyNode = function(n) {
88 n.getAttrs().forEach(function(attr) {
89 attrs[attr.name] = attr.value;
92 return node.document.createDocumentNode({
93 tagName: n.getTagName(),
98 var move = function(node, to) {
100 if(!node.containsNode(newNodes.second)) {
104 if(!node.sameNode(newNodes.second)) {
105 copy = to.append(copyNode(node));
106 node.contents().some(function(n) {
107 return move(n, copy);
115 newNodes.first.parents().some(function(p) {
116 if(p.getTagName() !== 'span') {
121 newNode = parent.before({tagName: parent.getTagName(), attrs: {'class': parent.getClass()}});
122 parent.contents().some(function(n) {
123 return move(n, newNode);
127 return _.extend(newNodes, {emptyText: emptyText});
130 mergeContentUp: function() {
136 if(myPrev.nodeType === Node.TEXT_NODE) {
137 if(myPrev.getIndex() > 0) {
140 myPrev = base = myPrev.parent();
143 myPrev = myPrev && myPrev.prev();
145 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
151 ret = myPrev.append(ptr);
161 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
166 plugin.documentExtension.documentNode.transformations = {
169 prev = toMerge.prev();
171 var merge = function(from, to) {
173 from.contents().forEach(function(node, idx) {
175 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
176 len = node.getText().length;
178 ret = to.append(node);
180 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
183 offset: ret.getText().length - len
187 node: ret.getFirstTextNode(),
198 applies: function() {
199 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
202 var textNode = prev.getLastTextNode(),
203 txt, prevText, prevTextLen;
205 txt = textNode.getText();
207 textNode.setText(txt.substr(0, txt.length-1));
208 return {node: toMerge, offset: 0};
210 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
211 prevTextLen = prevText.getText().length;
215 node: prevText ? prevText : toMerge,
216 offset : prevText ? prevTextLen : 0
223 applies: function() {
224 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
227 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
228 return merge(toMerge, prev);
230 if(prev && prev.is('list')) {
231 var items = prev.contents().filter(function(n) { return n.is('item');});
232 return merge(toMerge, items[items.length-1]);
237 applies: function() {
238 return toMerge.is({tagName: 'span'});
242 var toret = {node: toMerge.contents()[0] , offset: 0},
243 txt, txtNode, parent;
245 toMerge.parents().some(function(p) {
246 if(p.is({tagName: 'span'})) {
252 prev = prev && prev.prev();
258 return parent.moveUp();
260 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
261 prev.setText(txt.substr(0, txt.length-1));
263 } else if(prev.is({tagName: 'span'})) {
264 if((txtNode = prev.getLastTextNode())) {
265 txt = txtNode.getText();
267 txtNode.setText(txt.substr(0, txt.length-1));
269 if(txtNode.parent().contents().length === 1) {
270 txtNode.parent().detach();
283 applies: function() {
284 return toMerge.is({tagName: 'header'});
287 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
288 return merge(toMerge, prev);
293 applies: function() {
294 return toMerge.is('item');
298 if(prev && prev.is('item')) {
299 return merge(toMerge, prev);
300 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
301 list.before(toMerge);
302 toMerge.setClass('p');
303 if(!list.contents().length) {
306 return {node: toMerge.contents()[0], offset:0};
313 strategies.some(function(strategy) {
314 if(strategy.applies()) {
315 toret = strategy.run();
321 insertNewNode: function () {
323 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
324 node.after(newElement);
325 newElement.append({text: ''});
330 var undoRedoAction = function(dir) {
334 document: {type: 'context', name: 'document'},
337 label: dir === 'undo' ? '<-' : '->',
339 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
340 execute: function(callback, params) {
341 var metadata = _.last(params.document[dir+'Stack']).metadata,
342 fragment = metadata && metadata.fragment;
343 params.document[dir]();
345 if(!fragment.isValid()) {
346 fragment.restoreFromPaths();
348 if(fragment.isValid()) {
355 getState: function(params) {
356 var allowed = params.document && !!(params.document[dir+'Stack'].length),
357 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
358 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
360 var metadata = _.last(params.document[dir+'Stack']).metadata;
362 desc += ': ' + (metadata.description || gettext('unknown operation'));
367 description: allowed ? desc : descEmpty
373 var pad = function(number) {
375 number = '0' + number;
380 var commentAction = {
383 fragment: {type: 'context', name: 'fragment'}
387 execute: function(callback, params, editor) {
389 var node = params.fragment.node,
391 if(node.nodeType === Node.TEXT_NODE) {
392 node = node.parent();
394 node.document.transaction(function() {
395 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
396 comment.append({text:''});
397 var user = editor.getUser(), creator;
401 creator += ' (' + user.email + ')';
404 creator = 'anonymous';
407 var currentDate = new Date(),
408 dt = pad(currentDate.getDate()) + '-' +
409 pad((currentDate.getMonth() + 1)) + '-' +
410 pad(currentDate.getFullYear()) + ' ' +
411 pad(currentDate.getHours()) + ':' +
412 pad(currentDate.getMinutes()) + ':' +
413 pad(currentDate.getSeconds());
415 var metadata = comment.getMetadata();
416 metadata.add({key: 'creator', value: creator});
417 metadata.add({key: 'date', value: dt});
420 description: action.getState().description
426 getState: function(params) {
428 allowed: params.fragment && params.fragment.isValid() &&
429 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
432 state.description = gettext('Insert comment');
439 var createWrapTextAction = function(createParams) {
441 name: createParams.name,
443 fragment: {type: 'context', name: 'fragment'},
445 getState: function(params) {
447 label: this.config.label
451 if(!params.fragment || !params.fragment.isValid()) {
452 return _.extend(state, {allowed: false});
455 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
456 return _.extend(state, {
459 description: createParams.unwrapDescription,
460 execute: function(callback, params) {
461 var node = params.fragment.node,
463 toRemove = node.getParent(createParams.klass),
466 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
467 prefLen = toRemove.prev().getText().length;
470 doc.transaction(function() {
471 var ret = toRemove.unwrapContent(),
472 newFragment = params.fragment;
473 if(!newFragment.isValid()) {
474 newFragment = doc.createFragment(doc.CaretFragment, {
476 offset: prefLen + params.fragment.offset
482 description: createParams.unwrapDescription,
483 fragment: params.fragment
491 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundries()) {
492 parent = params.fragment.startNode.parent();
493 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
494 return _.extend(state, {allowed: false});
497 return _.extend(state, {
499 description: createParams.wrapDescription,
500 execute: function(callback, params) {
501 params.fragment.document.transaction(function() {
502 var parent = params.fragment.startNode.parent(),
503 doc = params.fragment.document,
504 wrapper, lastTextNode;
506 wrapper = parent.wrapText({
507 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
508 offsetStart: params.fragment.startOffset,
509 offsetEnd: params.fragment.endOffset,
510 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
513 lastTextNode = wrapper.getLastTextNode();
515 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
519 description: createParams.wrapDescription,
520 fragment: params.fragment
527 return _.extend(state, {allowed: false});
534 var createLinkFromSelection = function(callback, params) {
535 var doc = params.fragment.document,
536 dialog = Dialog.create({
537 title: gettext('Create link'),
538 executeButtonText: gettext('Apply'),
539 cancelButtonText: gettext('Cancel'),
541 {label: gettext('Link'), name: 'href', type: 'input',
542 prePasteHandler: function(text) {
543 return params.fragment.document.getLinkForUrl(text);
545 description: '<a href="#-" class="attachment-library">' + gettext('attachment library') + '</a>'
551 dialog.on('execute', function(event) {
552 doc.transaction(function() {
553 var span = action.params.fragment.startNode.parent().wrapText({
554 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
555 offsetStart: params.fragment.startOffset,
556 offsetEnd: params.fragment.endOffset,
557 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
559 doc = params.fragment.document;
561 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
564 description: action.getState().description,
565 fragment: params.fragment
571 $(".attachment-library", dialog.$el).on('click', function() {
572 attachments.select(function(v) {$("input", dialog.$el).val(v);});
576 var editLink = function(callback, params) {
577 var doc = params.fragment.document,
578 link = params.fragment.node.getParent('link'),
579 dialog = Dialog.create({
580 title: gettext('Edit link'),
581 executeButtonText: gettext('Apply'),
582 cancelButtonText: gettext('Cancel'),
584 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
589 dialog.on('execute', function(event) {
590 doc.transaction(function() {
591 link.setAttr('href', event.formData.href);
593 return params.fragment;
596 description: action.getState().description,
597 fragment: params.fragment
608 fragment: {type: 'context', name: 'fragment'}
611 label: gettext('link')
613 getState: function(params) {
614 if(!params.fragment || !params.fragment.isValid()) {
615 return {allowed: false};
618 if(params.fragment instanceof params.fragment.TextRangeFragment) {
619 if(!params.fragment.hasSiblingBoundries() || params.fragment.startNode.parent().is('link')) {
620 return {allowed: false};
624 description: gettext('Create link from selection'),
625 execute: createLinkFromSelection
629 if(params.fragment instanceof params.fragment.CaretFragment) {
630 if(params.fragment.node.isInside('link')) {
634 description: gettext('Edit link'),
639 return {allowed: false};
644 var metadataParams = {};
647 undoRedoAction('undo'),
648 undoRedoAction('redo'),
650 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
651 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
653 metadataEditor.action(metadataParams)
654 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
657 plugin.config = function(config) {
658 // templates.actions[0].config(config.templates);
659 templates.actions[0].params.template.options = config.templates;
660 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
661 if(configRow1.key < configRow2.key) {
664 if(configRow1.key > configRow2.key) {
671 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);