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 // span can't be the last node
126 newNode.append({'text': ''});
127 if(newNodes.second.contents()[0].getText().length === 0) {
128 var textNode = newNodes.second.contents()[0];
129 newNodes.second.detach();
130 newNodes.second = parent;
131 emptyText = newNodes.second.append(textNode);
135 var newNodeText = newNodes.second.contents()[0].getText();
136 if(newNodes.second.is({tagName: 'header'}) && newNodeText === '') {
137 newNodes.second = newNodes.second.setTag('div');
138 newNodes.second.setClass('p');
141 return _.extend(newNodes, {emptyText: emptyText});
144 mergeContentUp: function() {
150 if(myPrev.nodeType === Node.TEXT_NODE) {
151 if(myPrev.getIndex() > 0) {
154 myPrev = base = myPrev.parent();
157 myPrev = myPrev && myPrev.prev();
159 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
165 ret = myPrev.append(ptr);
175 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
180 plugin.documentExtension.documentNode.transformations = {
183 prev = toMerge.prev();
185 var merge = function(from, to) {
187 from.contents().forEach(function(node, idx) {
189 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
190 len = node.getText().length;
192 ret = to.append(node);
194 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
197 offset: ret.getText().length - len
201 node: ret.getFirstTextNode(),
212 applies: function() {
213 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
216 var textNode = prev.getLastTextNode(),
217 txt, prevText, prevTextLen;
219 txt = textNode.getText();
221 textNode.setText(txt.substr(0, txt.length-1));
222 return {node: toMerge, offset: 0};
224 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
225 prevTextLen = prevText.getText().length;
229 node: prevText ? prevText : toMerge,
230 offset : prevText ? prevTextLen : 0
237 applies: function() {
238 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
243 if(prev.is('p') || prev.is({tagName: 'header'})) {
244 return merge(toMerge, prev);
246 if(prev.is('list')) {
247 var items = prev.contents().filter(function(n) { return n.is('item');});
248 return merge(toMerge, items[items.length-1]);
253 applies: function() {
254 return toMerge.is({tagName: 'span'});
258 var toret = {node: toMerge.contents()[0] , offset: 0},
259 txt, txtNode, parent;
261 toMerge.parents().some(function(p) {
262 if(p.is({tagName: 'span'})) {
268 prev = prev && prev.prev();
274 return parent.moveUp();
276 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
277 prev.setText(txt.substr(0, txt.length-1));
279 } else if(prev.is({tagName: 'span'})) {
280 if((txtNode = prev.getLastTextNode())) {
281 txt = txtNode.getText();
283 txtNode.setText(txt.substr(0, txt.length-1));
285 if(txtNode.parent().contents().length === 1) {
286 txtNode.parent().detach();
299 applies: function() {
300 return toMerge.is({tagName: 'header'});
303 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
304 return merge(toMerge, prev);
309 applies: function() {
310 return toMerge.is('item');
314 if(prev && prev.is('item')) {
315 return merge(toMerge, prev);
316 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
317 list.before(toMerge);
318 toMerge.setClass('p');
319 if(!list.contents().length) {
322 return {node: toMerge.contents()[0], offset:0};
329 strategies.some(function(strategy) {
330 if(strategy.applies()) {
331 toret = strategy.run();
337 insertNewNode: function () {
339 var newElement = this.document.createDocumentNode({tagName: 'div', attrs: {class: 'p'}});
340 node.after(newElement);
341 newElement.append({text: ''});
346 var undoRedoAction = function(dir) {
350 document: {type: 'context', name: 'document'},
353 label: dir === 'undo' ? '<-' : '->',
355 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
356 execute: function(callback, params) {
357 var metadata = _.last(params.document[dir+'Stack']).metadata,
358 fragment = metadata && metadata.fragment;
359 params.document[dir]();
361 if(!fragment.isValid()) {
362 fragment.restoreFromPaths();
364 if(fragment.isValid()) {
371 getState: function(params) {
372 var allowed = params.document && !!(params.document[dir+'Stack'].length),
373 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
374 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
376 var metadata = _.last(params.document[dir+'Stack']).metadata;
378 desc += ': ' + (metadata.description || gettext('unknown operation'));
383 description: allowed ? desc : descEmpty
389 var pad = function(number) {
391 number = '0' + number;
396 var commentAction = {
399 fragment: {type: 'context', name: 'fragment'}
403 execute: function(callback, params, editor) {
405 var node = params.fragment.node,
407 if(node.nodeType === Node.TEXT_NODE) {
408 node = node.parent();
410 node.document.transaction(function() {
411 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
412 comment.append({text:''});
413 var user = editor.getUser(), creator;
417 creator += ' (' + user.email + ')';
420 creator = 'anonymous';
423 var currentDate = new Date(),
424 dt = pad(currentDate.getDate()) + '-' +
425 pad((currentDate.getMonth() + 1)) + '-' +
426 pad(currentDate.getFullYear()) + ' ' +
427 pad(currentDate.getHours()) + ':' +
428 pad(currentDate.getMinutes()) + ':' +
429 pad(currentDate.getSeconds());
431 var metadata = comment.getMetadata();
432 metadata.add({key: 'creator', value: creator});
433 metadata.add({key: 'date', value: dt});
436 description: action.getState().description
442 getState: function(params) {
444 allowed: params.fragment && params.fragment.isValid() &&
445 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
448 state.description = gettext('Insert comment');
455 var createWrapTextAction = function(createParams) {
457 name: createParams.name,
459 fragment: {type: 'context', name: 'fragment'},
461 getState: function(params) {
463 label: this.config.label
467 if(!params.fragment || !params.fragment.isValid()) {
468 return _.extend(state, {allowed: false});
471 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
472 return _.extend(state, {
475 description: createParams.unwrapDescription,
476 execute: function(callback, params) {
477 var node = params.fragment.node,
479 toRemove = node.getParent(createParams.klass),
482 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
483 prefLen = toRemove.prev().getText().length;
486 doc.transaction(function() {
487 var ret = toRemove.unwrapContent(),
488 newFragment = params.fragment;
489 if(!newFragment.isValid()) {
490 newFragment = doc.createFragment(doc.CaretFragment, {
492 offset: prefLen + params.fragment.offset
498 description: createParams.unwrapDescription,
499 fragment: params.fragment
507 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundaries()) {
508 parent = params.fragment.startNode.parent();
509 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
510 return _.extend(state, {allowed: false});
513 return _.extend(state, {
515 description: createParams.wrapDescription,
516 execute: function(callback, params) {
517 params.fragment.document.transaction(function() {
518 var parent = params.fragment.startNode.parent(),
519 doc = params.fragment.document,
520 wrapper, lastTextNode;
522 wrapper = parent.wrapText({
523 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
524 offsetStart: params.fragment.startOffset,
525 offsetEnd: params.fragment.endOffset,
526 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
528 if(wrapper.next() === null) {
529 // span can't be the last node
530 parent.append({text: ''});
533 lastTextNode = wrapper.getLastTextNode();
535 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
539 description: createParams.wrapDescription,
540 fragment: params.fragment
547 return _.extend(state, {allowed: false});
554 var createLinkFromSelection = function(callback, params) {
555 var doc = params.fragment.document,
556 dialog = Dialog.create({
557 title: gettext('Create link'),
558 executeButtonText: gettext('Apply'),
559 cancelButtonText: gettext('Cancel'),
561 {label: gettext('Link'), name: 'href', type: 'input',
562 prePasteHandler: function(text) {
563 return params.fragment.document.getLinkForUrl(text);
565 description: '<a href="#-" class="attachment-library">' + gettext('attachment library') + '</a>'
571 dialog.on('execute', function(event) {
572 doc.transaction(function() {
573 var span = action.params.fragment.startNode.parent().wrapText({
574 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
575 offsetStart: params.fragment.startOffset,
576 offsetEnd: params.fragment.endOffset,
577 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
579 doc = params.fragment.document;
581 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
584 description: action.getState().description,
585 fragment: params.fragment
591 $(".attachment-library", dialog.$el).on('click', function() {
592 attachments.select(function(v) {$("input", dialog.$el).val(v);});
596 var editLink = function(callback, params) {
597 var doc = params.fragment.document,
598 link = params.fragment.node.getParent('link'),
599 dialog = Dialog.create({
600 title: gettext('Edit link'),
601 executeButtonText: gettext('Apply'),
602 cancelButtonText: gettext('Cancel'),
604 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
609 dialog.on('execute', function(event) {
610 doc.transaction(function() {
611 link.setAttr('href', event.formData.href);
613 return params.fragment;
616 description: action.getState().description,
617 fragment: params.fragment
628 fragment: {type: 'context', name: 'fragment'}
631 label: gettext('link')
633 getState: function(params) {
634 if(!params.fragment || !params.fragment.isValid()) {
635 return {allowed: false};
638 if(params.fragment instanceof params.fragment.TextRangeFragment) {
639 if(!params.fragment.hasSiblingBoundaries() || params.fragment.startNode.parent().is('link')) {
640 return {allowed: false};
644 description: gettext('Create link from selection'),
645 execute: createLinkFromSelection
649 if(params.fragment instanceof params.fragment.CaretFragment) {
650 if(params.fragment.node.isInside('link')) {
654 description: gettext('Edit link'),
659 return {allowed: false};
664 var metadataParams = {};
667 undoRedoAction('undo'),
668 undoRedoAction('redo'),
670 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
671 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
673 metadataEditor.action(metadataParams)
674 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
677 plugin.config = function(config) {
678 // templates.actions[0].config(config.templates);
679 templates.actions[0].params.template.options = config.templates;
680 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
681 if(configRow1.key < configRow2.key) {
684 if(configRow1.key > configRow2.key) {
691 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);