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');
18 var exerciseFix = function(newNodes) {
19 var list, exercise, max, addedItem, answerValues;
20 if(newNodes.created.is('item')) {
21 list = newNodes.created.parent();
22 exercise = list.parent();
23 if(exercise && exercise.is('exercise')) {
24 if(exercise.is('exercise.order')) {
25 answerValues = exercise.object.getItems()
27 if(!addedItem && item.node.sameNode(newNodes.created)) {
30 return item.getAnswer();
32 max = Math.max.apply(Math.max, answerValues);
33 addedItem.setAnswer(max + 1);
39 plugin.documentExtension.textNode.transformations = {
41 impl: function(args) {
43 isSpan = node.parent().getTagName() === 'span',
44 parentDescribingNodes = [],
46 newNodes = node.split({offset: args.offset});
47 newNodes.second.contents()
48 .filter(function(child) {
49 return child.object.describesParent;
51 .forEach(function(child) {
53 parentDescribingNodes.push(child);
56 [newNodes.first, newNodes.second].some(function(newNode) {
57 if(!(newNode.contents().length)) {
58 emptyText = newNode.append({text: ''});
65 This makes sure that adding a new item to the list in some of the edumed exercises
66 sets an answer attribute that makes sense (and not just copies it which would create
69 This won't be neccessary when/if we introduce canvas element own key event handlers.
71 Alternatively, WLXML elements could implement their own item split methods that we
74 exerciseFix(newNodes);
77 parentDescribingNodes.forEach(function(node) {
78 newNodes.first.append(node);
83 var copyNode = function(n) {
85 n.getAttrs().forEach(function(attr) {
86 attrs[attr.name] = attr.value;
89 return node.document.createDocumentNode({
90 tagName: n.getTagName(),
95 var move = function(node, to) {
97 if(!node.containsNode(newNodes.second)) {
101 if(!node.sameNode(newNodes.second)) {
102 copy = to.append(copyNode(node));
103 node.contents().some(function(n) {
104 return move(n, copy);
112 newNodes.first.parents().some(function(p) {
113 if(p.getTagName() !== 'span') {
118 newNode = parent.before({tagName: parent.getTagName(), attrs: {'class': parent.getClass()}});
119 parent.contents().some(function(n) {
120 return move(n, newNode);
124 return _.extend(newNodes, {emptyText: emptyText});
127 mergeContentUp: function() {
133 if(myPrev.nodeType === Node.TEXT_NODE) {
134 if(myPrev.getIndex() > 0) {
137 myPrev = base = myPrev.parent();
140 myPrev = myPrev && myPrev.prev();
142 if(myPrev && myPrev.nodeType === Node.ELEMENT_NODE) {
148 ret = myPrev.append(ptr);
158 return {node: ret, offset: ret.sameNode(this) ? null : ret.getText().length - this.getText().length};
163 plugin.documentExtension.documentNode.transformations = {
166 prev = toMerge.prev();
168 var merge = function(from, to) {
170 from.contents().forEach(function(node, idx) {
172 if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
173 len = node.getText().length;
175 ret = to.append(node);
177 if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
180 offset: ret.getText().length - len
184 node: ret.getFirstTextNode(),
195 applies: function() {
196 return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
199 var textNode = prev.getLastTextNode(),
200 txt, prevText, prevTextLen;
202 txt = textNode.getText();
204 textNode.setText(txt.substr(0, txt.length-1));
205 return {node: toMerge, offset: 0};
207 if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
208 prevTextLen = prevText.getText().length;
212 node: prevText ? prevText : toMerge,
213 offset : prevText ? prevTextLen : 0
220 applies: function() {
221 return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
224 if(prev && (prev.is('p') || prev.is({tagName: 'header'}))) {
225 return merge(toMerge, prev);
227 if(prev && prev.is('list')) {
228 var items = prev.contents().filter(function(n) { return n.is('item');});
229 return merge(toMerge, items[items.length-1]);
234 applies: function() {
235 return toMerge.is({tagName: 'span'});
239 var toret = {node: toMerge.contents()[0] , offset: 0},
240 txt, txtNode, parent;
242 toMerge.parents().some(function(p) {
243 if(p.is({tagName: 'span'})) {
249 prev = prev && prev.prev();
255 return parent.moveUp();
257 else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
258 prev.setText(txt.substr(0, txt.length-1));
260 } else if(prev.is({tagName: 'span'})) {
261 if((txtNode = prev.getLastTextNode())) {
262 txt = txtNode.getText();
264 txtNode.setText(txt.substr(0, txt.length-1));
266 if(txtNode.parent().contents().length === 1) {
267 txtNode.parent().detach();
280 applies: function() {
281 return toMerge.is({tagName: 'header'});
284 if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
285 return merge(toMerge, prev);
290 applies: function() {
291 return toMerge.is('item');
295 if(prev && prev.is('item')) {
296 return merge(toMerge, prev);
297 } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
298 list.before(toMerge);
299 toMerge.setClass('p');
300 if(!list.contents().length) {
303 return {node: toMerge.contents()[0], offset:0};
310 strategies.some(function(strategy) {
311 if(strategy.applies()) {
312 toret = strategy.run();
320 var undoRedoAction = function(dir) {
324 document: {type: 'context', name: 'document'},
327 label: dir === 'undo' ? '<-' : '->',
329 iconStyle: dir === 'undo' ? '-webkit-transform: scale(-1,1); transform: scale(-1, 1)' : '',
330 execute: function(callback, params) {
331 var metadata = _.last(params.document[dir+'Stack']).metadata,
332 fragment = metadata && metadata.fragment;
333 params.document[dir]();
335 if(!fragment.isValid()) {
336 fragment.restoreFromPaths();
338 if(fragment.isValid()) {
345 getState: function(params) {
346 var allowed = params.document && !!(params.document[dir+'Stack'].length),
347 desc = dir === 'undo' ? gettext('Undo') : gettext('Redo'),
348 descEmpty = dir === 'undo' ? gettext('There is nothing to undo') : gettext('There is nothing to redo');
350 var metadata = _.last(params.document[dir+'Stack']).metadata;
352 desc += ': ' + (metadata.description || gettext('unknown operation'));
357 description: allowed ? desc : descEmpty
363 var pad = function(number) {
365 number = '0' + number;
370 var commentAction = {
373 fragment: {type: 'context', name: 'fragment'}
377 execute: function(callback, params, editor) {
379 var node = params.fragment.node,
381 if(node.nodeType === Node.TEXT_NODE) {
382 node = node.parent();
384 node.document.transaction(function() {
385 var comment = node.after({tagName: 'aside', attrs: {'class': 'comment'}});
386 comment.append({text:''});
387 var user = editor.getUser(), creator;
391 creator += ' (' + user.email + ')';
394 creator = 'anonymous';
397 var currentDate = new Date(),
398 dt = pad(currentDate.getDate()) + '-' +
399 pad((currentDate.getMonth() + 1)) + '-' +
400 pad(currentDate.getFullYear()) + ' ' +
401 pad(currentDate.getHours()) + ':' +
402 pad(currentDate.getMinutes()) + ':' +
403 pad(currentDate.getSeconds());
405 var metadata = comment.getMetadata();
406 metadata.add({key: 'creator', value: creator});
407 metadata.add({key: 'date', value: dt});
410 description: action.getState().description
416 getState: function(params) {
418 allowed: params.fragment && params.fragment.isValid() &&
419 params.fragment instanceof params.fragment.NodeFragment && !params.fragment.node.isRoot()
422 state.description = gettext('Insert comment');
429 var createWrapTextAction = function(createParams) {
431 name: createParams.name,
433 fragment: {type: 'context', name: 'fragment'},
435 getState: function(params) {
437 label: this.config.label
441 if(!params.fragment || !params.fragment.isValid()) {
442 return _.extend(state, {allowed: false});
445 if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside(createParams.klass)) {
446 return _.extend(state, {
449 description: createParams.unwrapDescription,
450 execute: function(callback, params) {
451 var node = params.fragment.node,
453 toRemove = node.getParent(createParams.klass),
456 if(node.sameNode(toRemove.contents()[0]) && toRemove.isPrecededByTextNode()) {
457 prefLen = toRemove.prev().getText().length;
460 doc.transaction(function() {
461 var ret = toRemove.unwrapContent(),
462 newFragment = params.fragment;
463 if(!newFragment.isValid()) {
464 newFragment = doc.createFragment(doc.CaretFragment, {
466 offset: prefLen + params.fragment.offset
472 description: createParams.unwrapDescription,
473 fragment: params.fragment
481 if(params.fragment instanceof params.fragment.TextRangeFragment && params.fragment.hasSiblingBoundries()) {
482 parent = params.fragment.startNode.parent();
483 if(parent && parent.is(createParams.klass) || parent.isInside(createParams.klass)) {
484 return _.extend(state, {allowed: false});
487 return _.extend(state, {
489 description: createParams.wrapDescription,
490 execute: function(callback, params) {
491 params.fragment.document.transaction(function() {
492 var parent = params.fragment.startNode.parent(),
493 doc = params.fragment.document,
494 wrapper, lastTextNode;
496 wrapper = parent.wrapText({
497 _with: {tagName: 'span', attrs: {'class': createParams.klass}},
498 offsetStart: params.fragment.startOffset,
499 offsetEnd: params.fragment.endOffset,
500 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
503 lastTextNode = wrapper.getLastTextNode();
505 return doc.createFragment(doc.CaretFragment, {node: lastTextNode, offset: lastTextNode.getText().length});
509 description: createParams.wrapDescription,
510 fragment: params.fragment
517 return _.extend(state, {allowed: false});
523 var createLinkFromSelection = function(callback, params) {
524 var doc = params.fragment.document,
525 dialog = Dialog.create({
526 title: gettext('Create link'),
527 executeButtonText: gettext('Apply'),
528 cancelButtonText: gettext('Cancel'),
530 {label: gettext('Link'), name: 'href', type: 'input',
531 prePasteHandler: function(text) {
532 return params.fragment.document.getLinkForUrl(text);
539 dialog.on('execute', function(event) {
540 doc.transaction(function() {
541 var span = action.params.fragment.startNode.parent().wrapText({
542 _with: {tagName: 'span', attrs: {'class': 'link', href: event.formData.href }},
543 offsetStart: params.fragment.startOffset,
544 offsetEnd: params.fragment.endOffset,
545 textNodeIdx: [params.fragment.startNode.getIndex(), params.fragment.endNode.getIndex()]
547 doc = params.fragment.document;
549 return doc.createFragment(doc.CaretFragment, {node: span.contents()[0], offset:0});
552 description: action.getState().description,
553 fragment: params.fragment
561 var editLink = function(callback, params) {
562 var doc = params.fragment.document,
563 link = params.fragment.node.getParent('link'),
564 dialog = Dialog.create({
565 title: gettext('Edit link'),
566 executeButtonText: gettext('Apply'),
567 cancelButtonText: gettext('Cancel'),
569 {label: gettext('Link'), name: 'href', type: 'input', initialValue: link.getAttr('href')}
574 dialog.on('execute', function(event) {
575 doc.transaction(function() {
576 link.setAttr('href', event.formData.href);
578 return params.fragment;
581 description: action.getState().description,
582 fragment: params.fragment
593 fragment: {type: 'context', name: 'fragment'}
596 label: gettext('link')
598 getState: function(params) {
599 if(!params.fragment || !params.fragment.isValid()) {
600 return {allowed: false};
603 if(params.fragment instanceof params.fragment.TextRangeFragment) {
604 if(!params.fragment.hasSiblingBoundries() || params.fragment.startNode.parent().is('link')) {
605 return {allowed: false};
609 description: gettext('Create link from selection'),
610 execute: createLinkFromSelection
614 if(params.fragment instanceof params.fragment.CaretFragment) {
615 if(params.fragment.node.isInside('link')) {
619 description: gettext('Edit link'),
624 return {allowed: false};
628 var metadataParams = {};
631 undoRedoAction('undo'),
632 undoRedoAction('redo'),
634 createWrapTextAction({name: 'emphasis', klass: 'emp', wrapDescription: gettext('Mark as emphasized'), unwrapDescription: gettext('Remove emphasis')}),
635 createWrapTextAction({name: 'cite', klass: 'cite', wrapDescription: gettext('Mark as citation'), unwrapDescription: gettext('Remove citation')}),
637 metadataEditor.action(metadataParams)
638 ].concat(plugin.actions, templates.actions, footnote.actions, switchTo.actions, lists.actions, edumed.actions);
641 plugin.config = function(config) {
642 // templates.actions[0].config(config.templates);
643 templates.actions[0].params.template.options = config.templates;
644 metadataParams.config = (config.metadata || []).sort(function(configRow1, configRow2) {
645 if(configRow1.key < configRow2.key) {
648 if(configRow1.key > configRow2.key) {
655 plugin.canvasElements = canvasElements.concat(edumed.canvasElements);