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);
125 return _.extend(newNodes, {emptyText: emptyText});
128 mergeContentUp: function() {
134 if(myPrev.nodeType === Node.TEXT_NODE) {
135 if(myPrev.getIndex() > 0) {
138 myPrev = base = myPrev.parent();
141 myPrev = myPrev && myPrev.prev();
143 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
149 ret = myPrev.append(ptr);
159 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
164 plugin.documentExtension.documentNode.transformations = {
167 prev = toMerge.prev();
169 var merge = function(from, to) {
171 from.contents().forEach(function(node, idx) {
173 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
174 len = node.getText().length;
176 ret = to.append(node);
178 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
181 offset: ret.getText().length - len
185 node: ret.getFirstTextNode(),
196 applies: function() {
197 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
200 var textNode = prev.getLastTextNode(),
201 txt, prevText, prevTextLen;
203 txt = textNode.getText();
205 textNode.setText(txt.substr(0, txt.length-1));
206 return {node: toMerge, offset: 0};
208 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
209 prevTextLen = prevText.getText().length;
213 node: prevText ? prevText : toMerge,
214 offset : prevText ? prevTextLen : 0
221 applies: function() {
222 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
225 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
226 return merge(toMerge, prev);
228 if(prev && prev.is('list')) {
229 var items = prev.contents().filter(function(n) { return n.is('item');});
230 return merge(toMerge, items[items.length-1]);
235 applies: function() {
236 return toMerge.is({tagName: 'span'});
240 var toret = {node: toMerge.contents()[0] , offset: 0},
241 txt, txtNode, parent;
243 toMerge.parents().some(function(p) {
244 if(p.is({tagName: 'span'})) {
250 prev = prev && prev.prev();
256 return parent.moveUp();
258 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
259 prev.setText(txt.substr(0, txt.length-1));
261 } else if(prev.is({tagName: 'span'})) {
262 if((txtNode = prev.getLastTextNode())) {
263 txt = txtNode.getText();
265 txtNode.setText(txt.substr(0, txt.length-1));
267 if(txtNode.parent().contents().length === 1) {
268 txtNode.parent().detach();
281 applies: function() {
282 return toMerge.is({tagName: 'header'});
285 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
286 return merge(toMerge, prev);
291 applies: function() {
292 return toMerge.is('item');
296 if(prev && prev.is('item')) {
297 return merge(toMerge, prev);
298 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
299 list.before(toMerge);
300 toMerge.setClass('p');
301 if(!list.contents().length) {
304 return {node: toMerge.contents()[0], offset:0};
311 strategies.some(function(strategy) {
312 if(strategy.applies()) {
313 toret = strategy.run();
319 insertNewNode: function () {
321 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
322 node.after(newElement);
323 newElement.append({text: ''});
328 var undoRedoAction = function(dir) {
332 document: {type: 'context', name: 'document'},
335 label: dir === 'undo' ? '<-' : '->',
337 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
338 execute: function(callback, params) {
339 var metadata = _.last(params.document[dir+'Stack']).metadata,
340 fragment = metadata && metadata.fragment;
341 params.document[dir]();
343 if(!fragment.isValid()) {
344 fragment.restoreFromPaths();
346 if(fragment.isValid()) {
353 getState: function(params) {
354 var allowed = params.document && !!(params.document[dir+'Stack'].length),
355 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
356 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
358 var metadata = _.last(params.document[dir+'Stack']).metadata;
360 desc += ': ' + (metadata.description || gettext('unknown operation'));
365 description: allowed ? desc : descEmpty
371 var pad = function(number) {
373 number = '0' + number;
378 var commentAction = {
381 fragment: {type: 'context', name: 'fragment'}
385 execute: function(callback, params, editor) {
387 var node = params.fragment.node,
389 if(node.nodeType === Node.TEXT_NODE) {
390 node = node.parent();
392 node.document.transaction(function() {
393 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
394 comment.append({text:''});
395 var user = editor.getUser(), creator;
399 creator += ' (' + user.email + ')';
402 creator = 'anonymous';
405 var currentDate = new Date(),
406 dt = pad(currentDate.getDate()) + '-' +
407 pad((currentDate.getMonth() + 1)) + '-' +
408 pad(currentDate.getFullYear()) + ' ' +
409 pad(currentDate.getHours()) + ':' +
410 pad(currentDate.getMinutes()) + ':' +
411 pad(currentDate.getSeconds());
413 var metadata = comment.getMetadata();
414 metadata.add({key: 'creator', value: creator});
415 metadata.add({key: 'date', value: dt});
418 description: action.getState().description
424 getState: function(params) {
426 allowed: params.fragment && params.fragment.isValid() &&
427 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
430 state.description = gettext('Insert comment');
437 var createWrapTextAction = function(createParams) {
439 name: createParams.name,
441 fragment: {type: 'context', name: 'fragment'},
443 getState: function(params) {
445 label: this.config.label
449 if(!params.fragment || !params.fragment.isValid()) {
450 return _.extend(state, {allowed: false});
453 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
454 return _.extend(state, {
457 description: createParams.unwrapDescription,
458 execute: function(callback, params) {
459 var node = params.fragment.node,
461 toRemove = node.getParent(createParams.klass),
464 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
465 prefLen = toRemove.prev().getText().length;
468 doc.transaction(function() {
469 var ret = toRemove.unwrapContent(),
470 newFragment = params.fragment;
471 if(!newFragment.isValid()) {
472 newFragment = doc.createFragment(doc.CaretFragment, {
474 offset: prefLen + params.fragment.offset
480 description: createParams.unwrapDescription,
481 fragment: params.fragment
489 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
490 parent = params.fragment.startNode.parent();
491 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
492 return _.extend(state, {allowed: false});
495 return _.extend(state, {
497 description: createParams.wrapDescription,
498 execute: function(callback, params) {
499 params.fragment.document.transaction(function() {
500 var parent = params.fragment.startNode.parent(),
501 doc = params.fragment.document,
502 wrapper, lastTextNode;
504 wrapper = parent.wrapText({
505 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
506 offsetStart: params.fragment.startOffset,
507 offsetEnd: params.fragment.endOffset,
508 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
511 lastTextNode = wrapper.getLastTextNode();
513 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
517 description: createParams.wrapDescription,
518 fragment: params.fragment
525 return _.extend(state, {allowed: false});
531 var createLinkFromSelection = function(callback, params) {
532 var fragment = params.fragment,
533 doc = fragment.document,
534 text = fragment.startNode.nativeNode.data.substring(fragment.startOffset, fragment.endOffset),
536 if (text.indexOf('//') >= 0 && text.indexOf(' ') < 0) {
538 } else if (text.substr(0, 4) === 'www.' && text.indexOF(' ') < 0) {
539 url = 'http://' + text;
541 var dialog = Dialog.create({
542 title: gettext('Create link'),
543 executeButtonText: gettext('Apply'),
544 cancelButtonText: gettext('Cancel'),
546 {label: gettext('Link'), name: 'href', type: 'input', initialValue: url || '',
547 prePasteHandler: function(text) {
548 return params.fragment.document.getLinkForUrl(text);
555 dialog.on('execute', function(event) {
556 doc.transaction(function() {
557 var span = action.params.fragment.startNode.parent().wrapText({
558 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
559 offsetStart: params.fragment.startOffset,
560 offsetEnd: params.fragment.endOffset,
561 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
563 doc = params.fragment.document;
565 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
568 description: action.getState().description,
569 fragment: params.fragment
575 add_attachments(dialog);
578 var editLink = function(callback, params) {
579 var doc = params.fragment.document,
580 link = params.fragment.node.getParent('link'),
581 dialog = Dialog.create({
582 title: gettext('Edit link'),
583 executeButtonText: gettext('Apply'),
584 cancelButtonText: gettext('Cancel'),
586 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
591 dialog.on('execute', function(event) {
592 doc.transaction(function() {
593 link.setAttr('href', event.formData.href);
595 return params.fragment;
598 description: action.getState().description,
599 fragment: params.fragment
610 fragment: {type: 'context', name: 'fragment'}
613 label: gettext('link')
615 getState: function(params) {
616 if(!params.fragment || !params.fragment.isValid()) {
617 return {allowed: false};
620 if(params.fragment instanceof params.fragment.TextRangeFragment) {
621 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
622 return {allowed: false};
626 description: gettext('Create link from selection'),
627 execute: createLinkFromSelection
631 if(params.fragment instanceof params.fragment.CaretFragment) {
632 if(params.fragment.node.isInside('link')) {
636 description: gettext('Edit link'),
641 return {allowed: false};
645 var metadataParams = {};
648 undoRedoAction('undo'),
649 undoRedoAction('redo'),
651 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
652 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
654 metadataEditor.action(metadataParams)
655 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
658 plugin.config = function(config) {
659 // templates.actions[0].config(config.templates);
660 templates.actions[0].params.template.options = config.templates;
661 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
662 if(configRow1.key < configRow2.key) {
665 if(configRow1.key > configRow2.key) {
672 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);