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 fragment = params.fragment,
526 doc = fragment.document,
527 text = fragment.startNode.nativeNode.data.substring(fragment.startOffset, fragment.endOffset),
529 if (text.indexOf('//') >= 0 && text.indexOf(' ') < 0) {
531 } else if (text.substr(0, 4) === 'www.' && text.indexOF(' ') < 0) {
532 url = 'http://' + text;
534 var dialog = Dialog.create({
535 title: gettext('Create link'),
536 executeButtonText: gettext('Apply'),
537 cancelButtonText: gettext('Cancel'),
539 {label: gettext('Link'), name: 'href', type: 'input', initialValue: url || '',
540 prePasteHandler: function(text) {
541 return params.fragment.document.getLinkForUrl(text);
548 dialog.on('execute', function(event) {
549 doc.transaction(function() {
550 var span = action.params.fragment.startNode.parent().wrapText({
551 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
552 offsetStart: params.fragment.startOffset,
553 offsetEnd: params.fragment.endOffset,
554 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
556 doc = params.fragment.document;
558 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
561 description: action.getState().description,
562 fragment: params.fragment
568 add_attachments(dialog);
571 var editLink = function(callback, params) {
572 var doc = params.fragment.document,
573 link = params.fragment.node.getParent('link'),
574 dialog = Dialog.create({
575 title: gettext('Edit link'),
576 executeButtonText: gettext('Apply'),
577 cancelButtonText: gettext('Cancel'),
579 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
584 dialog.on('execute', function(event) {
585 doc.transaction(function() {
586 link.setAttr('href', event.formData.href);
588 return params.fragment;
591 description: action.getState().description,
592 fragment: params.fragment
603 fragment: {type: 'context', name: 'fragment'}
606 label: gettext('link')
608 getState: function(params) {
609 if(!params.fragment || !params.fragment.isValid()) {
610 return {allowed: false};
613 if(params.fragment instanceof params.fragment.TextRangeFragment) {
614 if(!params.fragment.hasSiblingBoundries() || params.fragment.startNode.parent().is('link')) {
615 return {allowed: false};
619 description: gettext('Create link from selection'),
620 execute: createLinkFromSelection
624 if(params.fragment instanceof params.fragment.CaretFragment) {
625 if(params.fragment.node.isInside('link')) {
629 description: gettext('Edit link'),
634 return {allowed: false};
638 var metadataParams = {};
641 undoRedoAction('undo'),
642 undoRedoAction('redo'),
644 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
645 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
647 metadataEditor.action(metadataParams)
648 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
651 plugin.config = function(config) {
652 // templates.actions[0].config(config.templates);
653 templates.actions[0].params.template.options = config.templates;
654 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
655 if(configRow1.key < configRow2.key) {
658 if(configRow1.key > configRow2.key) {
665 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);