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);
125 if(newNodes.second.contents()[0].getText().length === 0) {
126 newNodes.second.detach();
127 newNodes.second = parent;
128 emptyText = newNodes.second.append({text: '\u200b'}); // why? why is ZWS needed here?
132 return _.extend(newNodes, {emptyText: emptyText});
135 mergeContentUp: function() {
141 if(myPrev.nodeType === Node.TEXT_NODE) {
142 if(myPrev.getIndex() > 0) {
145 myPrev = base = myPrev.parent();
148 myPrev = myPrev && myPrev.prev();
150 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
156 ret = myPrev.append(ptr);
166 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
171 plugin.documentExtension.documentNode.transformations = {
174 prev = toMerge.prev();
176 var merge = function(from, to) {
178 from.contents().forEach(function(node, idx) {
180 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
181 len = node.getText().length;
183 ret = to.append(node);
185 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
188 offset: ret.getText().length - len
192 node: ret.getFirstTextNode(),
203 applies: function() {
204 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
207 var textNode = prev.getLastTextNode(),
208 txt, prevText, prevTextLen;
210 txt = textNode.getText();
212 textNode.setText(txt.substr(0, txt.length-1));
213 return {node: toMerge, offset: 0};
215 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
216 prevTextLen = prevText.getText().length;
220 node: prevText ? prevText : toMerge,
221 offset : prevText ? prevTextLen : 0
228 applies: function() {
229 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
232 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
233 return merge(toMerge, prev);
235 if(prev && prev.is('list')) {
236 var items = prev.contents().filter(function(n) { return n.is('item');});
237 return merge(toMerge, items[items.length-1]);
242 applies: function() {
243 return toMerge.is({tagName: 'span'});
247 var toret = {node: toMerge.contents()[0] , offset: 0},
248 txt, txtNode, parent;
250 toMerge.parents().some(function(p) {
251 if(p.is({tagName: 'span'})) {
257 prev = prev && prev.prev();
263 return parent.moveUp();
265 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
266 prev.setText(txt.substr(0, txt.length-1));
268 } else if(prev.is({tagName: 'span'})) {
269 if((txtNode = prev.getLastTextNode())) {
270 txt = txtNode.getText();
272 txtNode.setText(txt.substr(0, txt.length-1));
274 if(txtNode.parent().contents().length === 1) {
275 txtNode.parent().detach();
288 applies: function() {
289 return toMerge.is({tagName: 'header'});
292 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
293 return merge(toMerge, prev);
298 applies: function() {
299 return toMerge.is('item');
303 if(prev && prev.is('item')) {
304 return merge(toMerge, prev);
305 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
306 list.before(toMerge);
307 toMerge.setClass('p');
308 if(!list.contents().length) {
311 return {node: toMerge.contents()[0], offset:0};
318 strategies.some(function(strategy) {
319 if(strategy.applies()) {
320 toret = strategy.run();
326 insertNewNode: function () {
328 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
329 node.after(newElement);
330 newElement.append({text: ''});
335 var undoRedoAction = function(dir) {
339 document: {type: 'context', name: 'document'},
342 label: dir === 'undo' ? '<-' : '->',
344 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
345 execute: function(callback, params) {
346 var metadata = _.last(params.document[dir+'Stack']).metadata,
347 fragment = metadata && metadata.fragment;
348 params.document[dir]();
350 if(!fragment.isValid()) {
351 fragment.restoreFromPaths();
353 if(fragment.isValid()) {
360 getState: function(params) {
361 var allowed = params.document && !!(params.document[dir+'Stack'].length),
362 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
363 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
365 var metadata = _.last(params.document[dir+'Stack']).metadata;
367 desc += ': ' + (metadata.description || gettext('unknown operation'));
372 description: allowed ? desc : descEmpty
378 var pad = function(number) {
380 number = '0' + number;
385 var commentAction = {
388 fragment: {type: 'context', name: 'fragment'}
392 execute: function(callback, params, editor) {
394 var node = params.fragment.node,
396 if(node.nodeType === Node.TEXT_NODE) {
397 node = node.parent();
399 node.document.transaction(function() {
400 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
401 comment.append({text:''});
402 var user = editor.getUser(), creator;
406 creator += ' (' + user.email + ')';
409 creator = 'anonymous';
412 var currentDate = new Date(),
413 dt = pad(currentDate.getDate()) + '-' +
414 pad((currentDate.getMonth() + 1)) + '-' +
415 pad(currentDate.getFullYear()) + ' ' +
416 pad(currentDate.getHours()) + ':' +
417 pad(currentDate.getMinutes()) + ':' +
418 pad(currentDate.getSeconds());
420 var metadata = comment.getMetadata();
421 metadata.add({key: 'creator', value: creator});
422 metadata.add({key: 'date', value: dt});
425 description: action.getState().description
431 getState: function(params) {
433 allowed: params.fragment && params.fragment.isValid() &&
434 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
437 state.description = gettext('Insert comment');
444 var createWrapTextAction = function(createParams) {
446 name: createParams.name,
448 fragment: {type: 'context', name: 'fragment'},
450 getState: function(params) {
452 label: this.config.label
456 if(!params.fragment || !params.fragment.isValid()) {
457 return _.extend(state, {allowed: false});
460 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
461 return _.extend(state, {
464 description: createParams.unwrapDescription,
465 execute: function(callback, params) {
466 var node = params.fragment.node,
468 toRemove = node.getParent(createParams.klass),
471 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
472 prefLen = toRemove.prev().getText().length;
475 doc.transaction(function() {
476 var ret = toRemove.unwrapContent(),
477 newFragment = params.fragment;
478 if(!newFragment.isValid()) {
479 newFragment = doc.createFragment(doc.CaretFragment, {
481 offset: prefLen + params.fragment.offset
487 description: createParams.unwrapDescription,
488 fragment: params.fragment
496 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
497 parent = params.fragment.startNode.parent();
498 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
499 return _.extend(state, {allowed: false});
502 return _.extend(state, {
504 description: createParams.wrapDescription,
505 execute: function(callback, params) {
506 params.fragment.document.transaction(function() {
507 var parent = params.fragment.startNode.parent(),
508 doc = params.fragment.document,
509 wrapper, lastTextNode;
511 wrapper = parent.wrapText({
512 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
513 offsetStart: params.fragment.startOffset,
514 offsetEnd: params.fragment.endOffset,
515 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
518 lastTextNode = wrapper.getLastTextNode();
520 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
524 description: createParams.wrapDescription,
525 fragment: params.fragment
532 return _.extend(state, {allowed: false});
539 var createLinkFromSelection = function(callback, params) {
540 var doc = params.fragment.document,
541 dialog = Dialog.create({
542 title: gettext('Create link'),
543 executeButtonText: gettext('Apply'),
544 cancelButtonText: gettext('Cancel'),
546 {label: gettext('Link'), name: 'href', type: 'input',
547 prePasteHandler: function(text) {
548 return params.fragment.document.getLinkForUrl(text);
550 description: '<a href="#-" class="attachment-library">' + gettext('attachment library') + '</a>'
556 dialog.on('execute', function(event) {
557 doc.transaction(function() {
558 var span = action.params.fragment.startNode.parent().wrapText({
559 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
560 offsetStart: params.fragment.startOffset,
561 offsetEnd: params.fragment.endOffset,
562 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
564 doc = params.fragment.document;
566 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
569 description: action.getState().description,
570 fragment: params.fragment
576 $(".attachment-library", dialog.$el).on('click', function() {
577 attachments.select(function(v) {$("input", dialog.$el).val(v);});
581 var editLink = function(callback, params) {
582 var doc = params.fragment.document,
583 link = params.fragment.node.getParent('link'),
584 dialog = Dialog.create({
585 title: gettext('Edit link'),
586 executeButtonText: gettext('Apply'),
587 cancelButtonText: gettext('Cancel'),
589 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
594 dialog.on('execute', function(event) {
595 doc.transaction(function() {
596 link.setAttr('href', event.formData.href);
598 return params.fragment;
601 description: action.getState().description,
602 fragment: params.fragment
613 fragment: {type: 'context', name: 'fragment'}
616 label: gettext('link')
618 getState: function(params) {
619 if(!params.fragment || !params.fragment.isValid()) {
620 return {allowed: false};
623 if(params.fragment instanceof params.fragment.TextRangeFragment) {
624 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
625 return {allowed: false};
629 description: gettext('Create link from selection'),
630 execute: createLinkFromSelection
634 if(params.fragment instanceof params.fragment.CaretFragment) {
635 if(params.fragment.node.isInside('link')) {
639 description: gettext('Edit link'),
644 return {allowed: false};
649 var metadataParams = {};
652 undoRedoAction('undo'),
653 undoRedoAction('redo'),
655 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
656 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
658 metadataEditor.action(metadataParams)
659 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
662 plugin.config = function(config) {
663 // templates.actions[0].config(config.templates);
664 templates.actions[0].params.template.options = config.templates;
665 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
666 if(configRow1.key < configRow2.key) {
669 if(configRow1.key > configRow2.key) {
676 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);