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 attachments = require('views/attachments/attachments');
21 var exerciseFix = function(newNodes) {
22 var list, exercise, max, addedItem, answerValues;
23 if(newNodes.created.is('item')) {
24 list = newNodes.created.parent();
25 exercise = list.parent();
26 if(exercise && exercise.is('exercise')) {
27 if(exercise.is('exercise.order')) {
28 answerValues = exercise.object.getItems()
30 if(!addedItem && item.node.sameNode(newNodes.created)) {
33 return item.getAnswer();
35 max = Math.max.apply(Math.max, answerValues);
36 addedItem.setAnswer(max + 1);
42 plugin.documentExtension.textNode.transformations = {
44 impl: function(args) {
46 isSpan = node.parent().getTagName() === 'span',
47 parentDescribingNodes = [],
49 newNodes = node.split({offset: args.offset});
50 newNodes.second.contents()
51 .filter(function(child) {
52 return child.object.describesParent;
54 .forEach(function(child) {
56 parentDescribingNodes.push(child);
59 [newNodes.first, newNodes.second].some(function(newNode) {
60 if(!(newNode.contents().length)) {
61 emptyText = newNode.append({text: ''});
68 This makes sure that adding a new item to the list in some of the edumed exercises
69 sets an answer attribute that makes sense (and not just copies it which would create
72 This won't be neccessary when/if we introduce canvas element own key event handlers.
74 Alternatively, WLXML elements could implement their own item split methods that we
77 exerciseFix(newNodes);
80 parentDescribingNodes.forEach(function(node) {
81 newNodes.first.append(node);
86 var copyNode = function(n) {
88 n.getAttrs().forEach(function(attr) {
89 attrs[attr.name] = attr.value;
92 return node.document.createDocumentNode({
93 tagName: n.getTagName(),
98 var move = function(node, to) {
100 if(!node.containsNode(newNodes.second)) {
104 if(!node.sameNode(newNodes.second)) {
105 copy = to.append(copyNode(node));
106 node.contents().some(function(n) {
107 return move(n, copy);
115 newNodes.first.parents().some(function(p) {
116 if(p.getTagName() !== 'span') {
121 newNode = parent.before({tagName: parent.getTagName(), attrs: {'class': parent.getClass()}});
122 parent.contents().some(function(n) {
123 return move(n, newNode);
127 return _.extend(newNodes, {emptyText: emptyText});
130 mergeContentUp: function() {
136 if(myPrev.nodeType === Node.TEXT_NODE) {
137 if(myPrev.getIndex() > 0) {
140 myPrev = base = myPrev.parent();
143 myPrev = myPrev && myPrev.prev();
145 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
151 ret = myPrev.append(ptr);
161 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
166 plugin.documentExtension.documentNode.transformations = {
169 prev = toMerge.prev();
171 var merge = function(from, to) {
173 from.contents().forEach(function(node, idx) {
175 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
176 len = node.getText().length;
178 ret = to.append(node);
180 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
183 offset: ret.getText().length - len
187 node: ret.getFirstTextNode(),
198 applies: function() {
199 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
202 var textNode = prev.getLastTextNode(),
203 txt, prevText, prevTextLen;
205 txt = textNode.getText();
207 textNode.setText(txt.substr(0, txt.length-1));
208 return {node: toMerge, offset: 0};
210 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
211 prevTextLen = prevText.getText().length;
215 node: prevText ? prevText : toMerge,
216 offset : prevText ? prevTextLen : 0
223 applies: function() {
224 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
227 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
228 return merge(toMerge, prev);
230 if(prev && prev.is('list')) {
231 var items = prev.contents().filter(function(n) { return n.is('item');});
232 return merge(toMerge, items[items.length-1]);
237 applies: function() {
238 return toMerge.is({tagName: 'span'});
242 var toret = {node: toMerge.contents()[0] , offset: 0},
243 txt, txtNode, parent;
245 toMerge.parents().some(function(p) {
246 if(p.is({tagName: 'span'})) {
252 prev = prev && prev.prev();
258 return parent.moveUp();
260 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
261 prev.setText(txt.substr(0, txt.length-1));
263 } else if(prev.is({tagName: 'span'})) {
264 if((txtNode = prev.getLastTextNode())) {
265 txt = txtNode.getText();
267 txtNode.setText(txt.substr(0, txt.length-1));
269 if(txtNode.parent().contents().length === 1) {
270 txtNode.parent().detach();
283 applies: function() {
284 return toMerge.is({tagName: 'header'});
287 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
288 return merge(toMerge, prev);
293 applies: function() {
294 return toMerge.is('item');
298 if(prev && prev.is('item')) {
299 return merge(toMerge, prev);
300 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
301 list.before(toMerge);
302 toMerge.setClass('p');
303 if(!list.contents().length) {
306 return {node: toMerge.contents()[0], offset:0};
313 strategies.some(function(strategy) {
314 if(strategy.applies()) {
315 toret = strategy.run();
323 var undoRedoAction = function(dir) {
327 document: {type: 'context', name: 'document'},
330 label: dir === 'undo' ? '<-' : '->',
332 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
333 execute: function(callback, params) {
334 var metadata = _.last(params.document[dir+'Stack']).metadata,
335 fragment = metadata && metadata.fragment;
336 params.document[dir]();
338 if(!fragment.isValid()) {
339 fragment.restoreFromPaths();
341 if(fragment.isValid()) {
348 getState: function(params) {
349 var allowed = params.document && !!(params.document[dir+'Stack'].length),
350 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
351 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
353 var metadata = _.last(params.document[dir+'Stack']).metadata;
355 desc += ': ' + (metadata.description || gettext('unknown operation'));
360 description: allowed ? desc : descEmpty
366 var pad = function(number) {
368 number = '0' + number;
373 var commentAction = {
376 fragment: {type: 'context', name: 'fragment'}
380 execute: function(callback, params, editor) {
382 var node = params.fragment.node,
384 if(node.nodeType === Node.TEXT_NODE) {
385 node = node.parent();
387 node.document.transaction(function() {
388 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
389 comment.append({text:''});
390 var user = editor.getUser(), creator;
394 creator += ' (' + user.email + ')';
397 creator = 'anonymous';
400 var currentDate = new Date(),
401 dt = pad(currentDate.getDate()) + '-' +
402 pad((currentDate.getMonth() + 1)) + '-' +
403 pad(currentDate.getFullYear()) + ' ' +
404 pad(currentDate.getHours()) + ':' +
405 pad(currentDate.getMinutes()) + ':' +
406 pad(currentDate.getSeconds());
408 var metadata = comment.getMetadata();
409 metadata.add({key: 'creator', value: creator});
410 metadata.add({key: 'date', value: dt});
413 description: action.getState().description
419 getState: function(params) {
421 allowed: params.fragment && params.fragment.isValid() &&
422 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
425 state.description = gettext('Insert comment');
432 var createWrapTextAction = function(createParams) {
434 name: createParams.name,
436 fragment: {type: 'context', name: 'fragment'},
438 getState: function(params) {
440 label: this.config.label
444 if(!params.fragment || !params.fragment.isValid()) {
445 return _.extend(state, {allowed: false});
448 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
449 return _.extend(state, {
452 description: createParams.unwrapDescription,
453 execute: function(callback, params) {
454 var node = params.fragment.node,
456 toRemove = node.getParent(createParams.klass),
459 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
460 prefLen = toRemove.prev().getText().length;
463 doc.transaction(function() {
464 var ret = toRemove.unwrapContent(),
465 newFragment = params.fragment;
466 if(!newFragment.isValid()) {
467 newFragment = doc.createFragment(doc.CaretFragment, {
469 offset: prefLen + params.fragment.offset
475 description: createParams.unwrapDescription,
476 fragment: params.fragment
484 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundries()) {
485 parent = params.fragment.startNode.parent();
486 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
487 return _.extend(state, {allowed: false});
490 return _.extend(state, {
492 description: createParams.wrapDescription,
493 execute: function(callback, params) {
494 params.fragment.document.transaction(function() {
495 var parent = params.fragment.startNode.parent(),
496 doc = params.fragment.document,
497 wrapper, lastTextNode;
499 wrapper = parent.wrapText({
500 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
501 offsetStart: params.fragment.startOffset,
502 offsetEnd: params.fragment.endOffset,
503 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
506 lastTextNode = wrapper.getLastTextNode();
508 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
512 description: createParams.wrapDescription,
513 fragment: params.fragment
520 return _.extend(state, {allowed: false});
527 var createLinkFromSelection = function(callback, params) {
528 var doc = params.fragment.document,
529 dialog = Dialog.create({
530 title: gettext('Create link'),
531 executeButtonText: gettext('Apply'),
532 cancelButtonText: gettext('Cancel'),
534 {label: gettext('Link'), name: 'href', type: 'input',
535 prePasteHandler: function(text) {
536 return params.fragment.document.getLinkForUrl(text);
538 description: '<a href="#-" class="attachment-library">attachment library</a>'
544 dialog.on('execute', function(event) {
545 doc.transaction(function() {
546 var span = action.params.fragment.startNode.parent().wrapText({
547 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
548 offsetStart: params.fragment.startOffset,
549 offsetEnd: params.fragment.endOffset,
550 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
552 doc = params.fragment.document;
554 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
557 description: action.getState().description,
558 fragment: params.fragment
564 $(".attachment-library", dialog.$el).on('click', function() {
565 attachments.select(function(v) {$("input", dialog.$el).val(v);});
569 var editLink = function(callback, params) {
570 var doc = params.fragment.document,
571 link = params.fragment.node.getParent('link'),
572 dialog = Dialog.create({
573 title: gettext('Edit link'),
574 executeButtonText: gettext('Apply'),
575 cancelButtonText: gettext('Cancel'),
577 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
582 dialog.on('execute', function(event) {
583 doc.transaction(function() {
584 link.setAttr('href', event.formData.href);
586 return params.fragment;
589 description: action.getState().description,
590 fragment: params.fragment
601 fragment: {type: 'context', name: 'fragment'}
604 label: gettext('link')
606 getState: function(params) {
607 if(!params.fragment || !params.fragment.isValid()) {
608 return {allowed: false};
611 if(params.fragment instanceof params.fragment.TextRangeFragment) {
612 if(!params.fragment.hasSiblingBoundries() || params.fragment.startNode.parent().is('link')) {
613 return {allowed: false};
617 description: gettext('Create link from selection'),
618 execute: createLinkFromSelection
622 if(params.fragment instanceof params.fragment.CaretFragment) {
623 if(params.fragment.node.isInside('link')) {
627 description: gettext('Edit link'),
632 return {allowed: false};
637 var metadataParams = {};
640 undoRedoAction('undo'),
641 undoRedoAction('redo'),
643 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
644 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
646 metadataEditor.action(metadataParams)
647 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
650 plugin.config = function(config) {
651 // templates.actions[0].config(config.templates);
652 templates.actions[0].params.template.options = config.templates;
653 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
654 if(configRow1.key < configRow2.key) {
657 if(configRow1.key > configRow2.key) {
664 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);