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);
125 if(newNodes.second.contents()[0].getText().length === 0) {
126 var textNode = newNodes.second.contents()[0];
127 newNodes.second.detach();
128 newNodes.second = parent;
129 emptyText = newNodes.second.append(textNode);
133 var newNodeText = newNodes.second.contents()[0].getText();
134 if(newNodes.second.is({tagName: 'header'}) && newNodeText === '') {
135 newNodes.second = newNodes.second.setTag('div');
136 newNodes.second.setClass('p');
139 return _.extend(newNodes, {emptyText: emptyText});
142 mergeContentUp: function() {
148 if(myPrev.nodeType === Node.TEXT_NODE) {
149 if(myPrev.getIndex() > 0) {
152 myPrev = base = myPrev.parent();
155 myPrev = myPrev && myPrev.prev();
157 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
163 ret = myPrev.append(ptr);
173 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
178 plugin.documentExtension.documentNode.transformations = {
181 prev = toMerge.prev();
183 var merge = function(from, to) {
185 from.contents().forEach(function(node, idx) {
187 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
188 len = node.getText().length;
190 ret = to.append(node);
192 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
195 offset: ret.getText().length - len
199 node: ret.getFirstTextNode(),
210 applies: function() {
211 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
214 var textNode = prev.getLastTextNode(),
215 txt, prevText, prevTextLen;
217 txt = textNode.getText();
219 textNode.setText(txt.substr(0, txt.length-1));
220 return {node: toMerge, offset: 0};
222 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
223 prevTextLen = prevText.getText().length;
227 node: prevText ? prevText : toMerge,
228 offset : prevText ? prevTextLen : 0
235 applies: function() {
236 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
239 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
240 return merge(toMerge, prev);
242 if(prev && prev.is('list')) {
243 var items = prev.contents().filter(function(n) { return n.is('item');});
244 return merge(toMerge, items[items.length-1]);
249 applies: function() {
250 return toMerge.is({tagName: 'span'});
254 var toret = {node: toMerge.contents()[0] , offset: 0},
255 txt, txtNode, parent;
257 toMerge.parents().some(function(p) {
258 if(p.is({tagName: 'span'})) {
264 prev = prev && prev.prev();
270 return parent.moveUp();
272 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
273 prev.setText(txt.substr(0, txt.length-1));
275 } else if(prev.is({tagName: 'span'})) {
276 if((txtNode = prev.getLastTextNode())) {
277 txt = txtNode.getText();
279 txtNode.setText(txt.substr(0, txt.length-1));
281 if(txtNode.parent().contents().length === 1) {
282 txtNode.parent().detach();
295 applies: function() {
296 return toMerge.is({tagName: 'header'});
299 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
300 return merge(toMerge, prev);
305 applies: function() {
306 return toMerge.is('item');
310 if(prev && prev.is('item')) {
311 return merge(toMerge, prev);
312 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
313 list.before(toMerge);
314 toMerge.setClass('p');
315 if(!list.contents().length) {
318 return {node: toMerge.contents()[0], offset:0};
325 strategies.some(function(strategy) {
326 if(strategy.applies()) {
327 toret = strategy.run();
333 insertNewNode: function () {
335 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
336 node.after(newElement);
337 newElement.append({text: ''});
342 var undoRedoAction = function(dir) {
346 document: {type: 'context', name: 'document'},
349 label: dir === 'undo' ? '<-' : '->',
351 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
352 execute: function(callback, params) {
353 var metadata = _.last(params.document[dir+'Stack']).metadata,
354 fragment = metadata && metadata.fragment;
355 params.document[dir]();
357 if(!fragment.isValid()) {
358 fragment.restoreFromPaths();
360 if(fragment.isValid()) {
367 getState: function(params) {
368 var allowed = params.document && !!(params.document[dir+'Stack'].length),
369 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
370 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
372 var metadata = _.last(params.document[dir+'Stack']).metadata;
374 desc += ': ' + (metadata.description || gettext('unknown operation'));
379 description: allowed ? desc : descEmpty
385 var pad = function(number) {
387 number = '0' + number;
392 var commentAction = {
395 fragment: {type: 'context', name: 'fragment'}
399 execute: function(callback, params, editor) {
401 var node = params.fragment.node,
403 if(node.nodeType === Node.TEXT_NODE) {
404 node = node.parent();
406 node.document.transaction(function() {
407 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
408 comment.append({text:''});
409 var user = editor.getUser(), creator;
413 creator += ' (' + user.email + ')';
416 creator = 'anonymous';
419 var currentDate = new Date(),
420 dt = pad(currentDate.getDate()) + '-' +
421 pad((currentDate.getMonth() + 1)) + '-' +
422 pad(currentDate.getFullYear()) + ' ' +
423 pad(currentDate.getHours()) + ':' +
424 pad(currentDate.getMinutes()) + ':' +
425 pad(currentDate.getSeconds());
427 var metadata = comment.getMetadata();
428 metadata.add({key: 'creator', value: creator});
429 metadata.add({key: 'date', value: dt});
432 description: action.getState().description
438 getState: function(params) {
440 allowed: params.fragment && params.fragment.isValid() &&
441 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
444 state.description = gettext('Insert comment');
451 var createWrapTextAction = function(createParams) {
453 name: createParams.name,
455 fragment: {type: 'context', name: 'fragment'},
457 getState: function(params) {
459 label: this.config.label
463 if(!params.fragment || !params.fragment.isValid()) {
464 return _.extend(state, {allowed: false});
467 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
468 return _.extend(state, {
471 description: createParams.unwrapDescription,
472 execute: function(callback, params) {
473 var node = params.fragment.node,
475 toRemove = node.getParent(createParams.klass),
478 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
479 prefLen = toRemove.prev().getText().length;
482 doc.transaction(function() {
483 var ret = toRemove.unwrapContent(),
484 newFragment = params.fragment;
485 if(!newFragment.isValid()) {
486 newFragment = doc.createFragment(doc.CaretFragment, {
488 offset: prefLen + params.fragment.offset
494 description: createParams.unwrapDescription,
495 fragment: params.fragment
503 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
504 parent = params.fragment.startNode.parent();
505 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
506 return _.extend(state, {allowed: false});
509 return _.extend(state, {
511 description: createParams.wrapDescription,
512 execute: function(callback, params) {
513 params.fragment.document.transaction(function() {
514 var parent = params.fragment.startNode.parent(),
515 doc = params.fragment.document,
516 wrapper, lastTextNode;
518 wrapper = parent.wrapText({
519 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
520 offsetStart: params.fragment.startOffset,
521 offsetEnd: params.fragment.endOffset,
522 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
525 lastTextNode = wrapper.getLastTextNode();
527 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
531 description: createParams.wrapDescription,
532 fragment: params.fragment
539 return _.extend(state, {allowed: false});
546 var createLinkFromSelection = function(callback, params) {
547 var doc = params.fragment.document,
548 dialog = Dialog.create({
549 title: gettext('Create link'),
550 executeButtonText: gettext('Apply'),
551 cancelButtonText: gettext('Cancel'),
553 {label: gettext('Link'), name: 'href', type: 'input',
554 prePasteHandler: function(text) {
555 return params.fragment.document.getLinkForUrl(text);
557 description: '<a href="#-" class="attachment-library">' + gettext('attachment library') + '</a>'
563 dialog.on('execute', function(event) {
564 doc.transaction(function() {
565 var span = action.params.fragment.startNode.parent().wrapText({
566 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
567 offsetStart: params.fragment.startOffset,
568 offsetEnd: params.fragment.endOffset,
569 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
571 doc = params.fragment.document;
573 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
576 description: action.getState().description,
577 fragment: params.fragment
583 $(".attachment-library", dialog.$el).on('click', function() {
584 attachments.select(function(v) {$("input", dialog.$el).val(v);});
588 var editLink = function(callback, params) {
589 var doc = params.fragment.document,
590 link = params.fragment.node.getParent('link'),
591 dialog = Dialog.create({
592 title: gettext('Edit link'),
593 executeButtonText: gettext('Apply'),
594 cancelButtonText: gettext('Cancel'),
596 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
601 dialog.on('execute', function(event) {
602 doc.transaction(function() {
603 link.setAttr('href', event.formData.href);
605 return params.fragment;
608 description: action.getState().description,
609 fragment: params.fragment
620 fragment: {type: 'context', name: 'fragment'}
623 label: gettext('link')
625 getState: function(params) {
626 if(!params.fragment || !params.fragment.isValid()) {
627 return {allowed: false};
630 if(params.fragment instanceof params.fragment.TextRangeFragment) {
631 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
632 return {allowed: false};
636 description: gettext('Create link from selection'),
637 execute: createLinkFromSelection
641 if(params.fragment instanceof params.fragment.CaretFragment) {
642 if(params.fragment.node.isInside('link')) {
646 description: gettext('Edit link'),
651 return {allowed: false};
656 var metadataParams = {};
659 undoRedoAction('undo'),
660 undoRedoAction('redo'),
662 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
663 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
665 metadataEditor.action(metadataParams)
666 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
669 plugin.config = function(config) {
670 // templates.actions[0].config(config.templates);
671 templates.actions[0].params.template.options = config.templates;
672 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
673 if(configRow1.key < configRow2.key) {
676 if(configRow1.key > configRow2.key) {
683 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);