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();
321 var undoRedoAction = function(dir) {
325 document: {type: 'context', name: 'document'},
328 label: dir === 'undo' ? '<-' : '->',
330 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
331 execute: function(callback, params) {
332 var metadata = _.last(params.document[dir+'Stack']).metadata,
333 fragment = metadata && metadata.fragment;
334 params.document[dir]();
336 if(!fragment.isValid()) {
337 fragment.restoreFromPaths();
339 if(fragment.isValid()) {
346 getState: function(params) {
347 var allowed = params.document && !!(params.document[dir+'Stack'].length),
348 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
349 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
351 var metadata = _.last(params.document[dir+'Stack']).metadata;
353 desc += ': ' + (metadata.description || gettext('unknown operation'));
358 description: allowed ? desc : descEmpty
364 var pad = function(number) {
366 number = '0' + number;
371 var commentAction = {
374 fragment: {type: 'context', name: 'fragment'}
378 execute: function(callback, params, editor) {
380 var node = params.fragment.node,
382 if(node.nodeType === Node.TEXT_NODE) {
383 node = node.parent();
385 node.document.transaction(function() {
386 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
387 comment.append({text:''});
388 var user = editor.getUser(), creator;
392 creator += ' (' + user.email + ')';
395 creator = 'anonymous';
398 var currentDate = new Date(),
399 dt = pad(currentDate.getDate()) + '-' +
400 pad((currentDate.getMonth() + 1)) + '-' +
401 pad(currentDate.getFullYear()) + ' ' +
402 pad(currentDate.getHours()) + ':' +
403 pad(currentDate.getMinutes()) + ':' +
404 pad(currentDate.getSeconds());
406 var metadata = comment.getMetadata();
407 metadata.add({key: 'creator', value: creator});
408 metadata.add({key: 'date', value: dt});
411 description: action.getState().description
417 getState: function(params) {
419 allowed: params.fragment && params.fragment.isValid() &&
420 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
423 state.description = gettext('Insert comment');
430 var createWrapTextAction = function(createParams) {
432 name: createParams.name,
434 fragment: {type: 'context', name: 'fragment'},
436 getState: function(params) {
438 label: this.config.label
442 if(!params.fragment || !params.fragment.isValid()) {
443 return _.extend(state, {allowed: false});
446 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
447 return _.extend(state, {
450 description: createParams.unwrapDescription,
451 execute: function(callback, params) {
452 var node = params.fragment.node,
454 toRemove = node.getParent(createParams.klass),
457 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
458 prefLen = toRemove.prev().getText().length;
461 doc.transaction(function() {
462 var ret = toRemove.unwrapContent(),
463 newFragment = params.fragment;
464 if(!newFragment.isValid()) {
465 newFragment = doc.createFragment(doc.CaretFragment, {
467 offset: prefLen + params.fragment.offset
473 description: createParams.unwrapDescription,
474 fragment: params.fragment
482 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundries()) {
483 parent = params.fragment.startNode.parent();
484 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
485 return _.extend(state, {allowed: false});
488 return _.extend(state, {
490 description: createParams.wrapDescription,
491 execute: function(callback, params) {
492 params.fragment.document.transaction(function() {
493 var parent = params.fragment.startNode.parent(),
494 doc = params.fragment.document,
495 wrapper, lastTextNode;
497 wrapper = parent.wrapText({
498 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
499 offsetStart: params.fragment.startOffset,
500 offsetEnd: params.fragment.endOffset,
501 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
504 lastTextNode = wrapper.getLastTextNode();
506 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
510 description: createParams.wrapDescription,
511 fragment: params.fragment
518 return _.extend(state, {allowed: false});
524 var createLinkFromSelection = function(callback, params) {
525 var doc = params.fragment.document,
526 dialog = Dialog.create({
527 title: gettext('Create link'),
528 executeButtonText: gettext('Apply'),
529 cancelButtonText: gettext('Cancel'),
531 {label: gettext('Link'), name: 'href', type: 'input',
532 prePasteHandler: function(text) {
533 return params.fragment.document.getLinkForUrl(text);
540 dialog.on('execute', function(event) {
541 doc.transaction(function() {
542 var span = action.params.fragment.startNode.parent().wrapText({
543 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
544 offsetStart: params.fragment.startOffset,
545 offsetEnd: params.fragment.endOffset,
546 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
548 doc = params.fragment.document;
550 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
553 description: action.getState().description,
554 fragment: params.fragment
560 add_attachments(dialog);
563 var editLink = function(callback, params) {
564 var doc = params.fragment.document,
565 link = params.fragment.node.getParent('link'),
566 dialog = Dialog.create({
567 title: gettext('Edit link'),
568 executeButtonText: gettext('Apply'),
569 cancelButtonText: gettext('Cancel'),
571 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
576 dialog.on('execute', function(event) {
577 doc.transaction(function() {
578 link.setAttr('href', event.formData.href);
580 return params.fragment;
583 description: action.getState().description,
584 fragment: params.fragment
595 fragment: {type: 'context', name: 'fragment'}
598 label: gettext('link')
600 getState: function(params) {
601 if(!params.fragment || !params.fragment.isValid()) {
602 return {allowed: false};
605 if(params.fragment instanceof params.fragment.TextRangeFragment) {
606 if(!params.fragment.hasSiblingBoundries() || params.fragment.startNode.parent().is('link')) {
607 return {allowed: false};
611 description: gettext('Create link from selection'),
612 execute: createLinkFromSelection
616 if(params.fragment instanceof params.fragment.CaretFragment) {
617 if(params.fragment.node.isInside('link')) {
621 description: gettext('Edit link'),
626 return {allowed: false};
630 var metadataParams = {};
633 undoRedoAction('undo'),
634 undoRedoAction('redo'),
636 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
637 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
639 metadataEditor.action(metadataParams)
640 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
643 plugin.config = function(config) {
644 // templates.actions[0].config(config.templates);
645 templates.actions[0].params.template.options = config.templates;
646 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
647 if(configRow1.key < configRow2.key) {
650 if(configRow1.key > configRow2.key) {
657 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);