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 newNodes.second.detach();
125 newNodes.second = parent;
126 emptyText = newNodes.second.append({text: '\u200b'}); // why? why is ZWS needed here?
130 return _.extend(newNodes, {emptyText: emptyText});
133 mergeContentUp: function() {
139 if(myPrev.nodeType === Node.TEXT_NODE) {
140 if(myPrev.getIndex() > 0) {
143 myPrev = base = myPrev.parent();
146 myPrev = myPrev && myPrev.prev();
148 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
154 ret = myPrev.append(ptr);
164 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
169 plugin.documentExtension.documentNode.transformations = {
172 prev = toMerge.prev();
174 var merge = function(from, to) {
176 from.contents().forEach(function(node, idx) {
178 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
179 len = node.getText().length;
181 ret = to.append(node);
183 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
186 offset: ret.getText().length - len
190 node: ret.getFirstTextNode(),
201 applies: function() {
202 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
205 var textNode = prev.getLastTextNode(),
206 txt, prevText, prevTextLen;
208 txt = textNode.getText();
210 textNode.setText(txt.substr(0, txt.length-1));
211 return {node: toMerge, offset: 0};
213 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
214 prevTextLen = prevText.getText().length;
218 node: prevText ? prevText : toMerge,
219 offset : prevText ? prevTextLen : 0
226 applies: function() {
227 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
230 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
231 return merge(toMerge, prev);
233 if(prev && prev.is('list')) {
234 var items = prev.contents().filter(function(n) { return n.is('item');});
235 return merge(toMerge, items[items.length-1]);
240 applies: function() {
241 return toMerge.is({tagName: 'span'});
245 var toret = {node: toMerge.contents()[0] , offset: 0},
246 txt, txtNode, parent;
248 toMerge.parents().some(function(p) {
249 if(p.is({tagName: 'span'})) {
255 prev = prev && prev.prev();
261 return parent.moveUp();
263 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
264 prev.setText(txt.substr(0, txt.length-1));
266 } else if(prev.is({tagName: 'span'})) {
267 if((txtNode = prev.getLastTextNode())) {
268 txt = txtNode.getText();
270 txtNode.setText(txt.substr(0, txt.length-1));
272 if(txtNode.parent().contents().length === 1) {
273 txtNode.parent().detach();
286 applies: function() {
287 return toMerge.is({tagName: 'header'});
290 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
291 return merge(toMerge, prev);
296 applies: function() {
297 return toMerge.is('item');
301 if(prev && prev.is('item')) {
302 return merge(toMerge, prev);
303 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
304 list.before(toMerge);
305 toMerge.setClass('p');
306 if(!list.contents().length) {
309 return {node: toMerge.contents()[0], offset:0};
316 strategies.some(function(strategy) {
317 if(strategy.applies()) {
318 toret = strategy.run();
324 insertNewNode: function () {
326 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
327 node.after(newElement);
328 newElement.append({text: ''});
333 var undoRedoAction = function(dir) {
337 document: {type: 'context', name: 'document'},
340 label: dir === 'undo' ? '<-' : '->',
342 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
343 execute: function(callback, params) {
344 var metadata = _.last(params.document[dir+'Stack']).metadata,
345 fragment = metadata && metadata.fragment;
346 params.document[dir]();
348 if(!fragment.isValid()) {
349 fragment.restoreFromPaths();
351 if(fragment.isValid()) {
358 getState: function(params) {
359 var allowed = params.document && !!(params.document[dir+'Stack'].length),
360 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
361 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
363 var metadata = _.last(params.document[dir+'Stack']).metadata;
365 desc += ': ' + (metadata.description || gettext('unknown operation'));
370 description: allowed ? desc : descEmpty
376 var pad = function(number) {
378 number = '0' + number;
383 var commentAction = {
386 fragment: {type: 'context', name: 'fragment'}
390 execute: function(callback, params, editor) {
392 var node = params.fragment.node,
394 if(node.nodeType === Node.TEXT_NODE) {
395 node = node.parent();
397 node.document.transaction(function() {
398 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
399 comment.append({text:''});
400 var user = editor.getUser(), creator;
404 creator += ' (' + user.email + ')';
407 creator = 'anonymous';
410 var currentDate = new Date(),
411 dt = pad(currentDate.getDate()) + '-' +
412 pad((currentDate.getMonth() + 1)) + '-' +
413 pad(currentDate.getFullYear()) + ' ' +
414 pad(currentDate.getHours()) + ':' +
415 pad(currentDate.getMinutes()) + ':' +
416 pad(currentDate.getSeconds());
418 var metadata = comment.getMetadata();
419 metadata.add({key: 'creator', value: creator});
420 metadata.add({key: 'date', value: dt});
423 description: action.getState().description
429 getState: function(params) {
431 allowed: params.fragment && params.fragment.isValid() &&
432 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
435 state.description = gettext('Insert comment');
442 var createWrapTextAction = function(createParams) {
444 name: createParams.name,
446 fragment: {type: 'context', name: 'fragment'},
448 getState: function(params) {
450 label: this.config.label
454 if(!params.fragment || !params.fragment.isValid()) {
455 return _.extend(state, {allowed: false});
458 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
459 return _.extend(state, {
462 description: createParams.unwrapDescription,
463 execute: function(callback, params) {
464 var node = params.fragment.node,
466 toRemove = node.getParent(createParams.klass),
469 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
470 prefLen = toRemove.prev().getText().length;
473 doc.transaction(function() {
474 var ret = toRemove.unwrapContent(),
475 newFragment = params.fragment;
476 if(!newFragment.isValid()) {
477 newFragment = doc.createFragment(doc.CaretFragment, {
479 offset: prefLen + params.fragment.offset
485 description: createParams.unwrapDescription,
486 fragment: params.fragment
494 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
495 parent = params.fragment.startNode.parent();
496 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
497 return _.extend(state, {allowed: false});
500 return _.extend(state, {
502 description: createParams.wrapDescription,
503 execute: function(callback, params) {
504 params.fragment.document.transaction(function() {
505 var parent = params.fragment.startNode.parent(),
506 doc = params.fragment.document,
507 wrapper, lastTextNode;
509 wrapper = parent.wrapText({
510 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
511 offsetStart: params.fragment.startOffset,
512 offsetEnd: params.fragment.endOffset,
513 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
516 lastTextNode = wrapper.getLastTextNode();
518 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
522 description: createParams.wrapDescription,
523 fragment: params.fragment
530 return _.extend(state, {allowed: false});
536 var createLinkFromSelection = function(callback, params) {
537 var fragment = params.fragment,
538 doc = fragment.document,
539 text = fragment.startNode.nativeNode.data.substring(fragment.startOffset, fragment.endOffset),
541 if (text.indexOf('//') >= 0 && text.indexOf(' ') < 0) {
543 } else if (text.substr(0, 4) === 'www.' && text.indexOF(' ') < 0) {
544 url = 'http://' + text;
546 var dialog = Dialog.create({
547 title: gettext('Create link'),
548 executeButtonText: gettext('Apply'),
549 cancelButtonText: gettext('Cancel'),
551 {label: gettext('Link'), name: 'href', type: 'input', initialValue: url || '',
552 prePasteHandler: function(text) {
553 return params.fragment.document.getLinkForUrl(text);
560 dialog.on('execute', function(event) {
561 doc.transaction(function() {
562 var span = action.params.fragment.startNode.parent().wrapText({
563 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
564 offsetStart: params.fragment.startOffset,
565 offsetEnd: params.fragment.endOffset,
566 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
568 doc = params.fragment.document;
570 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
573 description: action.getState().description,
574 fragment: params.fragment
580 add_attachments(dialog);
583 var editLink = function(callback, params) {
584 var doc = params.fragment.document,
585 link = params.fragment.node.getParent('link'),
586 dialog = Dialog.create({
587 title: gettext('Edit link'),
588 executeButtonText: gettext('Apply'),
589 cancelButtonText: gettext('Cancel'),
591 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
596 dialog.on('execute', function(event) {
597 doc.transaction(function() {
598 link.setAttr('href', event.formData.href);
600 return params.fragment;
603 description: action.getState().description,
604 fragment: params.fragment
615 fragment: {type: 'context', name: 'fragment'}
618 label: gettext('link')
620 getState: function(params) {
621 if(!params.fragment || !params.fragment.isValid()) {
622 return {allowed: false};
625 if(params.fragment instanceof params.fragment.TextRangeFragment) {
626 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
627 return {allowed: false};
631 description: gettext('Create link from selection'),
632 execute: createLinkFromSelection
636 if(params.fragment instanceof params.fragment.CaretFragment) {
637 if(params.fragment.node.isInside('link')) {
641 description: gettext('Edit link'),
646 return {allowed: false};
650 var metadataParams = {};
653 undoRedoAction('undo'),
654 undoRedoAction('redo'),
656 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
657 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
659 metadataEditor.action(metadataParams)
660 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
663 plugin.config = function(config) {
664 // templates.actions[0].config(config.templates);
665 templates.actions[0].params.template.options = config.templates;
666 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
667 if(configRow1.key < configRow2.key) {
670 if(configRow1.key > configRow2.key) {
677 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);