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 var textNode = newNodes.second.contents()[0];
127 newNodes.second.detach();
128 newNodes.second = parent;
129 emptyText = newNodes.second.append(textNode);
133 var newNodeText = newNodes.second.contents()[0].getText();
134 if(newNodes.second.is({tagName: 'header'}) && newNodeText === '') {
135 newNodes.second = newNodes.second.setTag('div');
136 newNodes.second.setClass('p');
139 return _.extend(newNodes, {emptyText: emptyText});
142 mergeContentUp: function() {
148 if(myPrev.nodeType === Node.TEXT_NODE) {
149 if(myPrev.getIndex() > 0) {
152 myPrev = base = myPrev.parent();
155 myPrev = myPrev && myPrev.prev();
157 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
163 ret = myPrev.append(ptr);
173 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
178 plugin.documentExtension.documentNode.transformations = {
181 prev = toMerge.prev();
183 var merge = function(from, to) {
185 from.contents().forEach(function(node, idx) {
187 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
188 len = node.getText().length;
190 ret = to.append(node);
192 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
195 offset: ret.getText().length - len
199 node: ret.getFirstTextNode(),
210 applies: function() {
211 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
214 var textNode = prev.getLastTextNode(),
215 txt, prevText, prevTextLen;
217 txt = textNode.getText();
219 textNode.setText(txt.substr(0, txt.length-1));
220 return {node: toMerge, offset: 0};
222 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
223 prevTextLen = prevText.getText().length;
227 node: prevText ? prevText : toMerge,
228 offset : prevText ? prevTextLen : 0
235 applies: function() {
236 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
241 if(prev.is('p') || prev.is({tagName: 'header'})) {
242 return merge(toMerge, prev);
244 if(prev.is('list')) {
245 var items = prev.contents().filter(function(n) { return n.is('item');});
246 return merge(toMerge, items[items.length-1]);
251 applies: function() {
252 return toMerge.is({tagName: 'span'});
256 var toret = {node: toMerge.contents()[0] , offset: 0},
257 txt, txtNode, parent;
259 toMerge.parents().some(function(p) {
260 if(p.is({tagName: 'span'})) {
266 prev = prev && prev.prev();
272 return parent.moveUp();
274 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
275 prev.setText(txt.substr(0, txt.length-1));
277 } else if(prev.is({tagName: 'span'})) {
278 if((txtNode = prev.getLastTextNode())) {
279 txt = txtNode.getText();
281 txtNode.setText(txt.substr(0, txt.length-1));
283 if(txtNode.parent().contents().length === 1) {
284 txtNode.parent().detach();
297 applies: function() {
298 return toMerge.is({tagName: 'header'});
301 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
302 return merge(toMerge, prev);
307 applies: function() {
308 return toMerge.is('item');
312 if(prev && prev.is('item')) {
313 return merge(toMerge, prev);
314 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
315 list.before(toMerge);
316 toMerge.setClass('p');
317 if(!list.contents().length) {
320 return {node: toMerge.contents()[0], offset:0};
327 strategies.some(function(strategy) {
328 if(strategy.applies()) {
329 toret = strategy.run();
335 insertNewNode: function () {
337 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
338 node.after(newElement);
339 newElement.append({text: ''});
344 var undoRedoAction = function(dir) {
348 document: {type: 'context', name: 'document'},
351 label: dir === 'undo' ? '<-' : '->',
353 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
354 execute: function(callback, params) {
355 var metadata = _.last(params.document[dir+'Stack']).metadata,
356 fragment = metadata && metadata.fragment;
357 params.document[dir]();
359 if(!fragment.isValid()) {
360 fragment.restoreFromPaths();
362 if(fragment.isValid()) {
369 getState: function(params) {
370 var allowed = params.document && !!(params.document[dir+'Stack'].length),
371 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
372 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
374 var metadata = _.last(params.document[dir+'Stack']).metadata;
376 desc += ': ' + (metadata.description || gettext('unknown operation'));
381 description: allowed ? desc : descEmpty
387 var pad = function(number) {
389 number = '0' + number;
394 var commentAction = {
397 fragment: {type: 'context', name: 'fragment'}
401 execute: function(callback, params, editor) {
403 var node = params.fragment.node,
405 if(node.nodeType === Node.TEXT_NODE) {
406 node = node.parent();
408 node.document.transaction(function() {
409 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
410 comment.append({text:''});
411 var user = editor.getUser(), creator;
415 creator += ' (' + user.email + ')';
418 creator = 'anonymous';
421 var currentDate = new Date(),
422 dt = pad(currentDate.getDate()) + '-' +
423 pad((currentDate.getMonth() + 1)) + '-' +
424 pad(currentDate.getFullYear()) + ' ' +
425 pad(currentDate.getHours()) + ':' +
426 pad(currentDate.getMinutes()) + ':' +
427 pad(currentDate.getSeconds());
429 var metadata = comment.getMetadata();
430 metadata.add({key: 'creator', value: creator});
431 metadata.add({key: 'date', value: dt});
434 description: action.getState().description
440 getState: function(params) {
442 allowed: params.fragment && params.fragment.isValid() &&
443 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
446 state.description = gettext('Insert comment');
453 var createWrapTextAction = function(createParams) {
455 name: createParams.name,
457 fragment: {type: 'context', name: 'fragment'},
459 getState: function(params) {
461 label: this.config.label
465 if(!params.fragment || !params.fragment.isValid()) {
466 return _.extend(state, {allowed: false});
469 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
470 return _.extend(state, {
473 description: createParams.unwrapDescription,
474 execute: function(callback, params) {
475 var node = params.fragment.node,
477 toRemove = node.getParent(createParams.klass),
480 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
481 prefLen = toRemove.prev().getText().length;
484 doc.transaction(function() {
485 var ret = toRemove.unwrapContent(),
486 newFragment = params.fragment;
487 if(!newFragment.isValid()) {
488 newFragment = doc.createFragment(doc.CaretFragment, {
490 offset: prefLen + params.fragment.offset
496 description: createParams.unwrapDescription,
497 fragment: params.fragment
505 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
506 parent = params.fragment.startNode.parent();
507 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
508 return _.extend(state, {allowed: false});
511 return _.extend(state, {
513 description: createParams.wrapDescription,
514 execute: function(callback, params) {
515 params.fragment.document.transaction(function() {
516 var parent = params.fragment.startNode.parent(),
517 doc = params.fragment.document,
518 wrapper, lastTextNode;
520 wrapper = parent.wrapText({
521 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
522 offsetStart: params.fragment.startOffset,
523 offsetEnd: params.fragment.endOffset,
524 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
527 lastTextNode = wrapper.getLastTextNode();
529 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
533 description: createParams.wrapDescription,
534 fragment: params.fragment
541 return _.extend(state, {allowed: false});
548 var createLinkFromSelection = function(callback, params) {
549 var doc = params.fragment.document,
550 dialog = Dialog.create({
551 title: gettext('Create link'),
552 executeButtonText: gettext('Apply'),
553 cancelButtonText: gettext('Cancel'),
555 {label: gettext('Link'), name: 'href', type: 'input',
556 prePasteHandler: function(text) {
557 return params.fragment.document.getLinkForUrl(text);
559 description: '<a href="#-" class="attachment-library">' + gettext('attachment library') + '</a>'
565 dialog.on('execute', function(event) {
566 doc.transaction(function() {
567 var span = action.params.fragment.startNode.parent().wrapText({
568 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
569 offsetStart: params.fragment.startOffset,
570 offsetEnd: params.fragment.endOffset,
571 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
573 doc = params.fragment.document;
575 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
578 description: action.getState().description,
579 fragment: params.fragment
585 $(".attachment-library", dialog.$el).on('click', function() {
586 attachments.select(function(v) {$("input", dialog.$el).val(v);});
590 var editLink = function(callback, params) {
591 var doc = params.fragment.document,
592 link = params.fragment.node.getParent('link'),
593 dialog = Dialog.create({
594 title: gettext('Edit link'),
595 executeButtonText: gettext('Apply'),
596 cancelButtonText: gettext('Cancel'),
598 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
603 dialog.on('execute', function(event) {
604 doc.transaction(function() {
605 link.setAttr('href', event.formData.href);
607 return params.fragment;
610 description: action.getState().description,
611 fragment: params.fragment
622 fragment: {type: 'context', name: 'fragment'}
625 label: gettext('link')
627 getState: function(params) {
628 if(!params.fragment || !params.fragment.isValid()) {
629 return {allowed: false};
632 if(params.fragment instanceof params.fragment.TextRangeFragment) {
633 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
634 return {allowed: false};
638 description: gettext('Create link from selection'),
639 execute: createLinkFromSelection
643 if(params.fragment instanceof params.fragment.CaretFragment) {
644 if(params.fragment.node.isInside('link')) {
648 description: gettext('Edit link'),
653 return {allowed: false};
658 var metadataParams = {};
661 undoRedoAction('undo'),
662 undoRedoAction('redo'),
664 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
665 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
667 metadataEditor.action(metadataParams)
668 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
671 plugin.config = function(config) {
672 // templates.actions[0].config(config.templates);
673 templates.actions[0].params.template.options = config.templates;
674 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
675 if(configRow1.key < configRow2.key) {
678 if(configRow1.key > configRow2.key) {
685 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);