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() === '');
237 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
238 return merge(toMerge, prev);
240 if(prev && prev.is('list')) {
241 var items = prev.contents().filter(function(n) { return n.is('item');});
242 return merge(toMerge, items[items.length-1]);
247 applies: function() {
248 return toMerge.is({tagName: 'span'});
252 var toret = {node: toMerge.contents()[0] , offset: 0},
253 txt, txtNode, parent;
255 toMerge.parents().some(function(p) {
256 if(p.is({tagName: 'span'})) {
262 prev = prev && prev.prev();
268 return parent.moveUp();
270 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
271 prev.setText(txt.substr(0, txt.length-1));
273 } else if(prev.is({tagName: 'span'})) {
274 if((txtNode = prev.getLastTextNode())) {
275 txt = txtNode.getText();
277 txtNode.setText(txt.substr(0, txt.length-1));
279 if(txtNode.parent().contents().length === 1) {
280 txtNode.parent().detach();
293 applies: function() {
294 return toMerge.is({tagName: 'header'});
297 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
298 return merge(toMerge, prev);
303 applies: function() {
304 return toMerge.is('item');
308 if(prev && prev.is('item')) {
309 return merge(toMerge, prev);
310 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
311 list.before(toMerge);
312 toMerge.setClass('p');
313 if(!list.contents().length) {
316 return {node: toMerge.contents()[0], offset:0};
323 strategies.some(function(strategy) {
324 if(strategy.applies()) {
325 toret = strategy.run();
331 insertNewNode: function () {
333 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
334 node.after(newElement);
335 newElement.append({text: ''});
340 var undoRedoAction = function(dir) {
344 document: {type: 'context', name: 'document'},
347 label: dir === 'undo' ? '<-' : '->',
349 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
350 execute: function(callback, params) {
351 var metadata = _.last(params.document[dir+'Stack']).metadata,
352 fragment = metadata && metadata.fragment;
353 params.document[dir]();
355 if(!fragment.isValid()) {
356 fragment.restoreFromPaths();
358 if(fragment.isValid()) {
365 getState: function(params) {
366 var allowed = params.document && !!(params.document[dir+'Stack'].length),
367 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
368 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
370 var metadata = _.last(params.document[dir+'Stack']).metadata;
372 desc += ': ' + (metadata.description || gettext('unknown operation'));
377 description: allowed ? desc : descEmpty
383 var pad = function(number) {
385 number = '0' + number;
390 var commentAction = {
393 fragment: {type: 'context', name: 'fragment'}
397 execute: function(callback, params, editor) {
399 var node = params.fragment.node,
401 if(node.nodeType === Node.TEXT_NODE) {
402 node = node.parent();
404 node.document.transaction(function() {
405 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
406 comment.append({text:''});
407 var user = editor.getUser(), creator;
411 creator += ' (' + user.email + ')';
414 creator = 'anonymous';
417 var currentDate = new Date(),
418 dt = pad(currentDate.getDate()) + '-' +
419 pad((currentDate.getMonth() + 1)) + '-' +
420 pad(currentDate.getFullYear()) + ' ' +
421 pad(currentDate.getHours()) + ':' +
422 pad(currentDate.getMinutes()) + ':' +
423 pad(currentDate.getSeconds());
425 var metadata = comment.getMetadata();
426 metadata.add({key: 'creator', value: creator});
427 metadata.add({key: 'date', value: dt});
430 description: action.getState().description
436 getState: function(params) {
438 allowed: params.fragment && params.fragment.isValid() &&
439 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
442 state.description = gettext('Insert comment');
449 var createWrapTextAction = function(createParams) {
451 name: createParams.name,
453 fragment: {type: 'context', name: 'fragment'},
455 getState: function(params) {
457 label: this.config.label
461 if(!params.fragment || !params.fragment.isValid()) {
462 return _.extend(state, {allowed: false});
465 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
466 return _.extend(state, {
469 description: createParams.unwrapDescription,
470 execute: function(callback, params) {
471 var node = params.fragment.node,
473 toRemove = node.getParent(createParams.klass),
476 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
477 prefLen = toRemove.prev().getText().length;
480 doc.transaction(function() {
481 var ret = toRemove.unwrapContent(),
482 newFragment = params.fragment;
483 if(!newFragment.isValid()) {
484 newFragment = doc.createFragment(doc.CaretFragment, {
486 offset: prefLen + params.fragment.offset
492 description: createParams.unwrapDescription,
493 fragment: params.fragment
501 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
502 parent = params.fragment.startNode.parent();
503 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
504 return _.extend(state, {allowed: false});
507 return _.extend(state, {
509 description: createParams.wrapDescription,
510 execute: function(callback, params) {
511 params.fragment.document.transaction(function() {
512 var parent = params.fragment.startNode.parent(),
513 doc = params.fragment.document,
514 wrapper, lastTextNode;
516 wrapper = parent.wrapText({
517 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
518 offsetStart: params.fragment.startOffset,
519 offsetEnd: params.fragment.endOffset,
520 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
523 lastTextNode = wrapper.getLastTextNode();
525 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
529 description: createParams.wrapDescription,
530 fragment: params.fragment
537 return _.extend(state, {allowed: false});
543 var createLinkFromSelection = function(callback, params) {
544 var fragment = params.fragment,
545 doc = fragment.document,
546 text = fragment.startNode.nativeNode.data.substring(fragment.startOffset, fragment.endOffset),
548 if (text.indexOf('//') >= 0 && text.indexOf(' ') < 0) {
550 } else if (text.substr(0, 4) === 'www.' && text.indexOF(' ') < 0) {
551 url = 'http://' + text;
553 var dialog = Dialog.create({
554 title: gettext('Create link'),
555 executeButtonText: gettext('Apply'),
556 cancelButtonText: gettext('Cancel'),
558 {label: gettext('Link'), name: 'href', type: 'input', initialValue: url || '',
559 prePasteHandler: function(text) {
560 return params.fragment.document.getLinkForUrl(text);
567 dialog.on('execute', function(event) {
568 doc.transaction(function() {
569 var span = action.params.fragment.startNode.parent().wrapText({
570 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
571 offsetStart: params.fragment.startOffset,
572 offsetEnd: params.fragment.endOffset,
573 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
575 doc = params.fragment.document;
577 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
580 description: action.getState().description,
581 fragment: params.fragment
587 add_attachments(dialog);
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};
657 var metadataParams = {};
660 undoRedoAction('undo'),
661 undoRedoAction('redo'),
663 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
664 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
666 metadataEditor.action(metadataParams)
667 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
670 plugin.config = function(config) {
671 // templates.actions[0].config(config.templates);
672 templates.actions[0].params.template.options = config.templates;
673 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
674 if(configRow1.key < configRow2.key) {
677 if(configRow1.key > configRow2.key) {
684 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);