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 // span can't be the last node
124 newNode.append({'text': ''});
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()]
526 if(wrapper.next() === null) {
527 // span can't be the last node
528 parent.append({text: ''});
531 lastTextNode = wrapper.getLastTextNode();
533 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
537 description: createParams.wrapDescription,
538 fragment: params.fragment
545 return _.extend(state, {allowed: false});
551 var createLinkFromSelection = function(callback, params) {
552 var fragment = params.fragment,
553 doc = fragment.document,
554 text = fragment.startNode.nativeNode.data.substring(fragment.startOffset, fragment.endOffset),
556 if (text.indexOf('//') >= 0 && text.indexOf(' ') < 0) {
558 } else if (text.substr(0, 4) === 'www.' && text.indexOF(' ') < 0) {
559 url = 'http://' + text;
561 var dialog = Dialog.create({
562 title: gettext('Create link'),
563 executeButtonText: gettext('Apply'),
564 cancelButtonText: gettext('Cancel'),
566 {label: gettext('Link'), name: 'href', type: 'input', initialValue: url || '',
567 prePasteHandler: function(text) {
568 return params.fragment.document.getLinkForUrl(text);
575 dialog.on('execute', function(event) {
576 doc.transaction(function() {
577 var span = action.params.fragment.startNode.parent().wrapText({
578 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
579 offsetStart: params.fragment.startOffset,
580 offsetEnd: params.fragment.endOffset,
581 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
583 doc = params.fragment.document;
585 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
588 description: action.getState().description,
589 fragment: params.fragment
595 add_attachments(dialog);
598 var editLink = function(callback, params) {
599 var doc = params.fragment.document,
600 link = params.fragment.node.getParent('link'),
601 dialog = Dialog.create({
602 title: gettext('Edit link'),
603 executeButtonText: gettext('Apply'),
604 cancelButtonText: gettext('Cancel'),
606 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
611 dialog.on('execute', function(event) {
612 doc.transaction(function() {
613 link.setAttr('href', event.formData.href);
615 return params.fragment;
618 description: action.getState().description,
619 fragment: params.fragment
630 fragment: {type: 'context', name: 'fragment'}
633 label: gettext('link')
635 getState: function(params) {
636 if(!params.fragment || !params.fragment.isValid()) {
637 return {allowed: false};
640 if(params.fragment instanceof params.fragment.TextRangeFragment) {
641 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
642 return {allowed: false};
646 description: gettext('Create link from selection'),
647 execute: createLinkFromSelection
651 if(params.fragment instanceof params.fragment.CaretFragment) {
652 if(params.fragment.node.isInside('link')) {
656 description: gettext('Edit link'),
661 return {allowed: false};
665 var metadataParams = {};
668 undoRedoAction('undo'),
669 undoRedoAction('redo'),
671 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
672 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
674 metadataEditor.action(metadataParams)
675 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
678 plugin.config = function(config) {
679 // templates.actions[0].config(config.templates);
680 templates.actions[0].params.template.options = config.templates;
681 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
682 if(configRow1.key < configRow2.key) {
685 if(configRow1.key > configRow2.key) {
692 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);