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 add_attachments = require('views/attachments/add_attachments');
19 var exerciseFix = function(newNodes) {
20 var list, exercise, max, addedItem, answerValues;
21 if(newNodes.created.is('item')) {
22 list = newNodes.created.parent();
23 exercise = list.parent();
24 if(exercise && exercise.is('exercise')) {
25 if(exercise.is('exercise.order')) {
26 answerValues = exercise.object.getItems()
28 if(!addedItem && item.node.sameNode(newNodes.created)) {
31 return item.getAnswer();
33 max = Math.max.apply(Math.max, answerValues);
34 addedItem.setAnswer(max + 1);
40 plugin.documentExtension.textNode.transformations = {
42 impl: function(args) {
44 isSpan = node.parent().getTagName() === 'span',
45 parentDescribingNodes = [],
47 newNodes = node.split({offset: args.offset});
48 newNodes.second.contents()
49 .filter(function(child) {
50 return child.object.describesParent;
52 .forEach(function(child) {
54 parentDescribingNodes.push(child);
57 [newNodes.first, newNodes.second].some(function(newNode) {
58 if(!(newNode.contents().length)) {
59 emptyText = newNode.append({text: ''});
66 This makes sure that adding a new item to the list in some of the edumed exercises
67 sets an answer attribute that makes sense (and not just copies it which would create
70 This won't be neccessary when/if we introduce canvas element own key event handlers.
72 Alternatively, WLXML elements could implement their own item split methods that we
75 exerciseFix(newNodes);
78 parentDescribingNodes.forEach(function(node) {
79 newNodes.first.append(node);
84 var copyNode = function(n) {
86 n.getAttrs().forEach(function(attr) {
87 attrs[attr.name] = attr.value;
90 return node.document.createDocumentNode({
91 tagName: n.getTagName(),
96 var move = function(node, to) {
98 if(!node.containsNode(newNodes.second)) {
102 if(!node.sameNode(newNodes.second)) {
103 copy = to.append(copyNode(node));
104 node.contents().some(function(n) {
105 return move(n, copy);
113 newNodes.first.parents().some(function(p) {
114 if(p.getTagName() !== 'span') {
119 newNode = parent.before({tagName: parent.getTagName(), attrs: {'class': parent.getClass()}});
120 parent.contents().some(function(n) {
121 return move(n, newNode);
123 if(newNodes.second.contents()[0].getText().length === 0) {
124 var textNode = newNodes.second.contents()[0];
125 newNodes.second.detach();
126 newNodes.second = parent;
127 emptyText = newNodes.second.append(textNode);
131 var newNodeText = newNodes.second.contents()[0].getText();
132 if(newNodes.second.is({tagName: 'header'}) && newNodeText === '') {
133 newNodes.second = newNodes.second.setTag('div');
134 newNodes.second.setClass('p');
137 return _.extend(newNodes, {emptyText: emptyText});
140 mergeContentUp: function() {
146 if(myPrev.nodeType === Node.TEXT_NODE) {
147 if(myPrev.getIndex() > 0) {
150 myPrev = base = myPrev.parent();
153 myPrev = myPrev && myPrev.prev();
155 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
161 ret = myPrev.append(ptr);
171 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
176 plugin.documentExtension.documentNode.transformations = {
179 prev = toMerge.prev();
181 var merge = function(from, to) {
183 from.contents().forEach(function(node, idx) {
185 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
186 len = node.getText().length;
188 ret = to.append(node);
190 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
193 offset: ret.getText().length - len
197 node: ret.getFirstTextNode(),
208 applies: function() {
209 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
212 var textNode = prev.getLastTextNode(),
213 txt, prevText, prevTextLen;
215 txt = textNode.getText();
217 textNode.setText(txt.substr(0, txt.length-1));
218 return {node: toMerge, offset: 0};
220 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
221 prevTextLen = prevText.getText().length;
225 node: prevText ? prevText : toMerge,
226 offset : prevText ? prevTextLen : 0
233 applies: function() {
234 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
239 if(prev.is('p') || prev.is({tagName: 'header'})) {
240 return merge(toMerge, prev);
242 if(prev.is('list')) {
243 var items = prev.contents().filter(function(n) { return n.is('item');});
244 return merge(toMerge, items[items.length-1]);
249 applies: function() {
250 return toMerge.is({tagName: 'span'});
254 var toret = {node: toMerge.contents()[0] , offset: 0},
255 txt, txtNode, parent;
257 toMerge.parents().some(function(p) {
258 if(p.is({tagName: 'span'})) {
264 prev = prev && prev.prev();
270 return parent.moveUp();
272 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
273 prev.setText(txt.substr(0, txt.length-1));
275 } else if(prev.is({tagName: 'span'})) {
276 if((txtNode = prev.getLastTextNode())) {
277 txt = txtNode.getText();
279 txtNode.setText(txt.substr(0, txt.length-1));
281 if(txtNode.parent().contents().length === 1) {
282 txtNode.parent().detach();
295 applies: function() {
296 return toMerge.is({tagName: 'header'});
299 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
300 return merge(toMerge, prev);
305 applies: function() {
306 return toMerge.is('item');
310 if(prev && prev.is('item')) {
311 return merge(toMerge, prev);
312 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
313 list.before(toMerge);
314 toMerge.setClass('p');
315 if(!list.contents().length) {
318 return {node: toMerge.contents()[0], offset:0};
325 strategies.some(function(strategy) {
326 if(strategy.applies()) {
327 toret = strategy.run();
333 insertNewNode: function () {
335 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
336 node.after(newElement);
337 newElement.append({text: ''});
342 var undoRedoAction = function(dir) {
346 document: {type: 'context', name: 'document'},
349 label: dir === 'undo' ? '<-' : '->',
351 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
352 execute: function(callback, params) {
353 var metadata = _.last(params.document[dir+'Stack']).metadata,
354 fragment = metadata && metadata.fragment;
355 params.document[dir]();
357 if(!fragment.isValid()) {
358 fragment.restoreFromPaths();
360 if(fragment.isValid()) {
367 getState: function(params) {
368 var allowed = params.document && !!(params.document[dir+'Stack'].length),
369 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
370 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
372 var metadata = _.last(params.document[dir+'Stack']).metadata;
374 desc += ': ' + (metadata.description || gettext('unknown operation'));
379 description: allowed ? desc : descEmpty
385 var pad = function(number) {
387 number = '0' + number;
392 var commentAction = {
395 fragment: {type: 'context', name: 'fragment'}
399 execute: function(callback, params, editor) {
401 var node = params.fragment.node,
403 if(node.nodeType === Node.TEXT_NODE) {
404 node = node.parent();
406 node.document.transaction(function() {
407 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
408 comment.append({text:''});
409 var user = editor.getUser(), creator;
413 creator += ' (' + user.email + ')';
416 creator = 'anonymous';
419 var currentDate = new Date(),
420 dt = pad(currentDate.getDate()) + '-' +
421 pad((currentDate.getMonth() + 1)) + '-' +
422 pad(currentDate.getFullYear()) + ' ' +
423 pad(currentDate.getHours()) + ':' +
424 pad(currentDate.getMinutes()) + ':' +
425 pad(currentDate.getSeconds());
427 var metadata = comment.getMetadata();
428 metadata.add({key: 'creator', value: creator});
429 metadata.add({key: 'date', value: dt});
432 description: action.getState().description
438 getState: function(params) {
440 allowed: params.fragment && params.fragment.isValid() &&
441 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
444 state.description = gettext('Insert comment');
451 var createWrapTextAction = function(createParams) {
453 name: createParams.name,
455 fragment: {type: 'context', name: 'fragment'},
457 getState: function(params) {
459 label: this.config.label
463 if(!params.fragment || !params.fragment.isValid()) {
464 return _.extend(state, {allowed: false});
467 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
468 return _.extend(state, {
471 description: createParams.unwrapDescription,
472 execute: function(callback, params) {
473 var node = params.fragment.node,
475 toRemove = node.getParent(createParams.klass),
478 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
479 prefLen = toRemove.prev().getText().length;
482 doc.transaction(function() {
483 var ret = toRemove.unwrapContent(),
484 newFragment = params.fragment;
485 if(!newFragment.isValid()) {
486 newFragment = doc.createFragment(doc.CaretFragment, {
488 offset: prefLen + params.fragment.offset
494 description: createParams.unwrapDescription,
495 fragment: params.fragment
503 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
504 parent = params.fragment.startNode.parent();
505 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
506 return _.extend(state, {allowed: false});
509 return _.extend(state, {
511 description: createParams.wrapDescription,
512 execute: function(callback, params) {
513 params.fragment.document.transaction(function() {
514 var parent = params.fragment.startNode.parent(),
515 doc = params.fragment.document,
516 wrapper, lastTextNode;
518 wrapper = parent.wrapText({
519 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
520 offsetStart: params.fragment.startOffset,
521 offsetEnd: params.fragment.endOffset,
522 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
525 lastTextNode = wrapper.getLastTextNode();
527 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
531 description: createParams.wrapDescription,
532 fragment: params.fragment
539 return _.extend(state, {allowed: false});
545 var createLinkFromSelection = function(callback, params) {
546 var fragment = params.fragment,
547 doc = fragment.document,
548 text = fragment.startNode.nativeNode.data.substring(fragment.startOffset, fragment.endOffset),
550 if (text.indexOf('//') >= 0 && text.indexOf(' ') < 0) {
552 } else if (text.substr(0, 4) === 'www.' && text.indexOF(' ') < 0) {
553 url = 'http://' + text;
555 var dialog = Dialog.create({
556 title: gettext('Create link'),
557 executeButtonText: gettext('Apply'),
558 cancelButtonText: gettext('Cancel'),
560 {label: gettext('Link'), name: 'href', type: 'input', initialValue: url || '',
561 prePasteHandler: function(text) {
562 return params.fragment.document.getLinkForUrl(text);
569 dialog.on('execute', function(event) {
570 doc.transaction(function() {
571 var span = action.params.fragment.startNode.parent().wrapText({
572 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
573 offsetStart: params.fragment.startOffset,
574 offsetEnd: params.fragment.endOffset,
575 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
577 doc = params.fragment.document;
579 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
582 description: action.getState().description,
583 fragment: params.fragment
589 add_attachments(dialog);
592 var editLink = function(callback, params) {
593 var doc = params.fragment.document,
594 link = params.fragment.node.getParent('link'),
595 dialog = Dialog.create({
596 title: gettext('Edit link'),
597 executeButtonText: gettext('Apply'),
598 cancelButtonText: gettext('Cancel'),
600 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
605 dialog.on('execute', function(event) {
606 doc.transaction(function() {
607 link.setAttr('href', event.formData.href);
609 return params.fragment;
612 description: action.getState().description,
613 fragment: params.fragment
624 fragment: {type: 'context', name: 'fragment'}
627 label: gettext('link')
629 getState: function(params) {
630 if(!params.fragment || !params.fragment.isValid()) {
631 return {allowed: false};
634 if(params.fragment instanceof params.fragment.TextRangeFragment) {
635 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
636 return {allowed: false};
640 description: gettext('Create link from selection'),
641 execute: createLinkFromSelection
645 if(params.fragment instanceof params.fragment.CaretFragment) {
646 if(params.fragment.node.isInside('link')) {
650 description: gettext('Edit link'),
655 return {allowed: false};
659 var metadataParams = {};
662 undoRedoAction('undo'),
663 undoRedoAction('redo'),
665 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
666 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
668 metadataEditor.action(metadataParams)
669 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
672 plugin.config = function(config) {
673 // templates.actions[0].config(config.templates);
674 templates.actions[0].params.template.options = config.templates;
675 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
676 if(configRow1.key < configRow2.key) {
679 if(configRow1.key > configRow2.key) {
686 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);