event.preventDefault();
node.setText('');
}
- else if(element.isEmpty()) {
- event.preventDefault();
- var parent = element.parent(),
- grandParent = parent ? parent.parent() : null;
- if(!grandParent && parent.children().length === 1) {
- return;
- }
- if(parent.children().length === 1 && parent.children()[0].sameNode(element)) {
- if(grandParent && grandParent.children().length === 1) {
- goto = grandParent.wlxmlNode.append({text: ''});
- } else {
- goto = canvas.getNearestTextElement(direction, element);
- }
- parent.wlxmlNode.detach();
- } else {
- goto = canvas.getNearestTextElement(direction, element);
- element.wlxmlNode.detach();
- }
- canvas.setCurrentElement(goto, {caretTo: caretTo});
- }
else if(cursorAtOperationEdge) {
if(direction === 'below') {
element = canvas.getNearestTextElement(direction, element);
}
- if(element) {
- goto = element.wlxmlNode.mergeContentUp();
+ if(element && element.wlxmlNode.getIndex() === 0) {
+ goto = element.wlxmlNode.parent().moveUp();
if(goto) {
canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
}
}
});
+var handleKeyEvent = function(e, s) {
+ keyEventHandlers.some(function(handler) {
+ if(handler.applies(e, s)) {
+ handler.run(e, s);
+ return true;
+ }
+ });
+};
+// todo: whileRemoveWholetext
+var keyEventHandlers = [
+ {
+ applies: function(e, s) {
+ return s.type === 'caret' && (
+ (s.isAtBeginning() && e.key === KEYS.BACKSPACE) ||
+ (s.isAtEnd() && e.key === KEYS.DELETE)
+ );
+ },
+ run: function(e,s) {
+ var direction, caretTo, cursorAtOperationEdge, goto, element;
+
+ if(e.key === KEYS.BACKSPACE) {
+ direction = 'above';
+ caretTo = 'end';
+ cursorAtOperationEdge = s.isAtBeginning();
+ element = s.element;
+ }
+ else {
+ direction = 'below';
+ caretTo = 'start';
+ cursorAtOperationEdge = s.isAtEnd();
+ if(cursorAtOperationEdge) {
+ element = cursorAtOperationEdge && s.canvas.getNearestTextElement(direction, s.element);
+ }
+ }
+
+ if(!cursorAtOperationEdge || !element) {
+ return;
+ }
+
+ e.preventDefault();
+
+ s.canvas.wlxmlDocument.transaction(function() {
+ if(element.wlxmlNode.getIndex() === 0) {
+ goto = element.wlxmlNode.parent().moveUp();
+ if(goto) {
+ s.canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
+ }
+ }
+ }, {
+ metadata: {
+ description: gettext('Remove text')
+ }
+ });
+ }
+ },
+
+ {
+ applies: function(e, s) {
+ return s.type === 'textSelection' && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
+ },
+ run: function(e, s) {
+ var direction = 'above',
+ caretTo = 'end',
+ goto;
+
+
+ if(e.key === KEYS.DELETE) {
+ direction = 'below';
+ caretTo = 'start';
+ }
+
+ e.preventDefault();
+ if(direction === 'above') {
+ if(s.startsAtBeginning()) {
+ goto = s.canvas.getNearestTextElement('above', s.startElement);
+ caretTo = 'end';
+ } else {
+ goto = s.startElement;
+ caretTo = s.startOffset;
+ }
+ } else {
+ if(s.endsAtEnd()) {
+ goto = s.canvas.getNearestTextElement('below', s.startElement);
+ caretTo = 'start';
+ } else {
+ goto = s.endElement;
+ caretTo = 0;
+ }
+ }
+
+ s.canvas.wlxmlDocument.deleteText({
+ from: {
+ node: s.startElement.wlxmlNode,
+ offset: s.startOffset
+ },
+ to: {
+ node: s.endElement.wlxmlNode,
+ offset: s.endOffset
+ }
+ });
+ if(goto) {
+ s.canvas.setCurrentElement(goto, {caretTo: caretTo});
+ }
+ }
+ }
+];
+
return {
- handleKey: handleKey
+ handleKey: handleKey,
+ handleKeyEvent: handleKeyEvent,
+ KEYS: KEYS
};
});
\ No newline at end of file
footnote = require('plugins/core/footnote'),
switchTo = require('plugins/core/switch'),
lists = require('plugins/core/lists'),
- plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}}},
+ plugin = {name: 'core', actions: [], canvas: {}, documentExtension: {textNode: {}, elementNode: {}}},
Dialog = require('views/dialog/dialog'),
canvasElements = require('plugins/core/canvasElements'),
metadataEditor = require('plugins/core/metadataEditor/metadataEditor');
}
};
+plugin.documentExtension.elementNode.transformations = {
+ moveUp: function() {
+ var toMerge = this,
+ prev = toMerge.prev();
+
+ var merge = function(from, to) {
+ from.contents().forEach(function(node) {
+ to.append(node);
+ });
+ from.detach();
+ };
+
+ var strategies = [
+ {
+ applies: function() {
+ return toMerge.is('p');
+ },
+ run: function() {
+ if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
+ merge(toMerge, prev);
+ }
+ if(prev && prev.is('list')) {
+ var items = prev.contents().filter(function(n) { return n.is('item');});
+ merge(toMerge, items[items.length-1]);
+ //return {node: toMerge, offset:0};
+ }
+ }
+ },
+ {
+ applies: function() {
+ return toMerge.is({tagName: 'header'});
+ },
+ run: function() {
+ if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
+ merge(toMerge, prev);
+ }
+ }
+ },
+ {
+ applies: function() {
+ return toMerge.is('item');
+ },
+ run: function() {
+ var list;
+ if(prev && prev.is('item')) {
+ merge(toMerge, prev);
+ } else if(!prev && (list = toMerge.parent()) && list.is('list')) {
+ list.before(toMerge);
+ toMerge.setClass('p');
+ if(!list.contents().length) {
+ list.detach();
+ }
+ return {node: toMerge.contents()[0], offset:0};
+ }
+ }
+ }
+ ];
+
+ var toret;
+ strategies.some(function(strategy) {
+ if(strategy.applies()) {
+ toret = strategy.run();
+ return true;
+ }
+ });
+ return toret;
+ }
+};
+
var undoRedoAction = function(dir) {
return {
name: dir,
'use strict';
/* globals describe, it */
-var chai = require('libs/chai'),
+var _ = require('libs/underscore'),
+ chai = require('libs/chai'),
sinon = require('libs/sinon'),
wlxml = require('wlxml/wlxml'),
+ canvas = require('modules/documentCanvas/canvas/canvas'),
+ keyboard = require('modules/documentCanvas/canvas/keyboard'),
+ keyEvent = require('modules/documentCanvas/canvas/keyEvent'),
corePlugin = require('./core.js'),
expect = chai.expect;
+var K = keyboard.KEYS;
+
var getDocumentFromXML = function(xml, options) {
var doc = wlxml.WLXMLDocumentFromXML(xml, options || {});
doc.registerExtension(corePlugin.documentExtension);
return doc;
};
+
+var getCanvasFromXML = function(xml, elements) {
+ return canvas.fromXMLDocument(getDocumentFromXML(xml), elements);
+};
+
var getTextNodes = function(text, doc) {
/* globals Node */
var toret = [];
return nodes[0];
};
+var getTextElement = function(text, c) {
+ var node = getTextNode(text, c.wlxmlDocument),
+ element = node && node.getData('canvasElement');
+ if(!(element && element.getText() === text)) {
+ throw new Error();
+ }
+ return element;
+};
+
describe('Document extensions', function() {
describe('break content', function() {
});
});
+describe.only('Keyboard interactions', function() {
+
+ var Keyboard = function(canvas) {
+ this.canvas = canvas;
+ };
+
+ _.extend(Keyboard.prototype, {
+ press: function(key) {
+ this.canvas.triggerKeyEvent(keyEvent.fromParams({key:key}), this.selection);
+ this.selection = this.canvas.getSelection();
+ return this;
+ },
+ withCaret: function(where) {
+ var offset = where.indexOf('|'),
+ text = where.split('|').join(''),
+ el = getTextElement(text, this.canvas),
+ selection = this.canvas.createSelection({type: 'caret', element: el, offset: offset});
+ if(offset === -1) {
+ throw new Error('Invalid caret');
+ }
+ this.selection = selection;
+ return this;
+ },
+ withSelection: function(start, end) {
+ var startOffset = start.indexOf('|'),
+ endOffset = end.indexOf('|'),
+ startText= start.split('|').join(''),
+ endText = end.split('|').join(''),
+ startElement = getTextElement(startText, this.canvas),
+ endElement = getTextElement(endText, this.canvas),
+ selection = this.canvas.createSelection({
+ type: 'textSelection',
+ anchorElement: startElement,
+ anchorOffset: startOffset,
+ focusElement: endElement,
+ focusOffset: endOffset
+ });
+ if(startOffset === -1 || endOffset === -1) {
+ throw new Error('Invalid text selection');
+ }
+ this.selection = selection;
+ return this;
+ }
+ });
+
+ describe('deleting text with selection', function() {
+ [K.BACKSPACE, K.DELETE].forEach(function(key) {
+ it('deletes text withing a single text element ' + key, function() {
+ var c = getCanvasFromXML('<section><div>Alice</div></section>'),
+ k = new Keyboard(c);
+
+ k.withSelection('A|lice', 'Alic|e').press(key);
+ expect(c.wlxmlDocument.root.contents()[0].contents()[0].getText()).to.equal('Ae');
+ });
+ it('deletes text across two paragraphs ' + key, function() {
+ var c = getCanvasFromXML('<section><div class="p">Alice</div><div class="p">cat</div></section>'),
+ k = new Keyboard(c);
+
+ k.withSelection('A|lice', 'c|at').press(key);
+ var rootContents = c.wlxmlDocument.root.contents();
+
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].contents()[0].getText()).to.equal('A');
+ expect(rootContents[1].contents()[0].getText()).to.equal('at');
+ });
+
+ it('keeps an empty paragraph after deleting its whole text ' + key, function() {
+ var c = getCanvasFromXML('<section><div class="p">Alice</div></section>'),
+ k = new Keyboard(c);
+
+ k.withSelection('|Alice', 'Alice|').press(key);
+ var rootContents = c.wlxmlDocument.root.contents();
+
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].contents()[0].getText()).to.equal('');
+ });
+ });
+
+ });
+
+ // describe('deleting with a caret', function() {
+ // it('keeps an empty paragraph after deleteing last letter with backspace', function() {
+ // var c = getCanvasFromXML('<section><div class="p">A</div></section>'),
+ // k = new Keyboard(c);
+
+ // k.withCaret('A|').press(K.BACKSPACE);
+ // var rootContents = c.wlxmlDocument.root.contents();
+
+ // expect(rootContents.length).to.equal(1);
+ // expect(rootContents[0].contents()[0].getText()).to.equal('');
+ // });
+ // // it('removes a paragraph on yet another delete' + key, function() {
+
+ // // });
+ // });
+
+
+ // + empty when bck/ins + l===1
+
+ describe('backspace at the beginning', function() {
+ it('merges two adjacent paragraphs', function() {
+ var c = getCanvasFromXML('<section><div class="p">A</div><div class="p">B</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|B').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].getClass()).to.equal('p');
+ expect(rootContents[0].contents()[0].getText()).to.equal('AB');
+ });
+ it('merges a paragraph with a header', function() {
+ var c = getCanvasFromXML('<section><header>A</header><div class="p">B</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|B').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].getTagName()).to.equal('header');
+ expect(rootContents[0].contents()[0].getText()).to.equal('AB');
+ });
+ it('merges two adjacent headers', function() {
+ var c = getCanvasFromXML('<section><header>A</header><header>B</header></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|B').press(K.BACKSPACE);
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].getTagName()).to.equal('header');
+ expect(rootContents[0].contents()[0].getText()).to.equal('AB');
+ });
+ it('merges a header with a paragraph', function() {
+ var c = getCanvasFromXML('<section><div class="p">A</div><header>B</header></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|B').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].is('p')).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('AB');
+ });
+ it('merges a paragraph into a last list item', function() {
+ var c = getCanvasFromXML('<section><div class="list"><div class="item">item</div></div><div class="p">paragraph</div></section>'),
+ list = c.wlxmlDocument.root.contents()[0],
+ k = new Keyboard(c);
+
+ k.withCaret('|paragraph').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].sameNode(list)).to.equal(true);
+
+ var items = list.contents();
+ expect(items.length).to.equal(1);
+ expect(items[0].contents()[0].getText()).to.equal('itemparagraph');
+ });
+ it('merges a list item with a list item', function() {
+ var c = getCanvasFromXML('<section><div class="list"><div class="item">item1</div><div class="item">item2</div></div></section>'),
+ list = c.wlxmlDocument.root.contents()[0],
+ k = new Keyboard(c);
+
+ k.withCaret('|item2').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+
+ expect(rootContents[0].sameNode(list)).to.equal(true);
+
+ var items = list.contents();
+
+ expect(items.length).to.equal(1);
+ expect(items[0].contents()[0].getText()).to.equal('item1item2');
+ });
+ it('creates a new paragraph preceding the list from a first list item', function() {
+ var c = getCanvasFromXML('<section><div class="list"><div class="item">item1</div><div class="item">item2</div></div></section>'),
+ list = c.wlxmlDocument.root.contents()[0],
+ k = new Keyboard(c);
+
+ k.withCaret('|item1').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+
+ expect(rootContents[0].getClass()).to.equal('p');
+ expect(rootContents[0].contents()[0].getText()).to.equal('item1');
+
+ expect(rootContents[1].sameNode(list)).to.equal(true);
+ });
+ it('removes list after moving up its only item', function() {
+ var c = getCanvasFromXML('<section><div class="list"><div class="item">item</div></div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|item').press(K.BACKSPACE);
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+
+ expect(rootContents[0].getClass()).to.equal('p');
+ expect(rootContents[0].contents()[0].getText()).to.equal('item');
+ });
+ });
+
+
+});
+
});
\ No newline at end of file