editor: wip partial
authorAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Wed, 30 Jul 2014 13:04:26 +0000 (15:04 +0200)
committerAleksander Łukasz <aleksander.lukasz@nowoczesnapolska.org.pl>
Thu, 14 Aug 2014 14:01:45 +0000 (16:01 +0200)
src/editor/modules/documentCanvas/canvas/canvas.js
src/editor/modules/documentCanvas/canvas/keyEvent.js [new file with mode: 0644]
src/editor/modules/documentCanvas/canvas/keyboard.js
src/editor/modules/documentCanvas/canvas/selection.js
src/editor/plugins/core/core.js
src/editor/plugins/core/core.test.js

index 6f648ec..9d33856 100644 (file)
@@ -146,6 +146,14 @@ $.extend(Canvas.prototype, Backbone.Events, {
         this.rootWrapper.append(this.rootElement.dom);
     },
 
+
+    triggerKeyEvent: function(keyEvent, selection) {
+        selection = selection || this.getSelection();
+        if(selection && (selection.type === 'caret' || selection.type === 'textSelection') && selection.toDocumentFragment().isValid()) {
+            keyboard.handleKeyEvent(keyEvent, selection);
+        }
+    },
+
     setupEventHandling: function() {
         var canvas = this;
 
@@ -350,6 +358,13 @@ $.extend(Canvas.prototype, Backbone.Events, {
         }
     },
 
+    setSelection: function(selection) {
+        this.select(this, selection.toDocumentFragment());
+    },
+
+    createSelection: function(params) {
+        return selection.fromParams(this, params);
+    },
     setCurrentElement: function(element, params) {
         if(!element) {
             logger.debug('Invalid element passed to setCurrentElement: ' + element);
diff --git a/src/editor/modules/documentCanvas/canvas/keyEvent.js b/src/editor/modules/documentCanvas/canvas/keyEvent.js
new file mode 100644 (file)
index 0000000..a5ce48b
--- /dev/null
@@ -0,0 +1,40 @@
+define(function(require) {
+    
+'use strict';
+
+var _ = require('libs/underscore'),
+    keyboard = require('modules/documentCanvas/canvas/keyboard');
+
+var KEYS = keyboard.KEYS;
+
+var KeyEvent = function(params) {
+    this.key = params.key;
+    this.ctrlKey = params.ctrlKey;
+    this._nativeEvent = params._nativeEvent;
+};
+
+_.extend(KeyEvent.prototype, KEYS, {
+    forKey: function(k) {
+        return k === this.key;
+    },
+    preventDefault: function() {
+        if(this._nativeEvent) {
+            this._nativeEvent.preventDefault();
+        }
+    }
+});
+
+return {
+    fromParams: function(params) {
+        return new KeyEvent(params);
+    },
+    fromNativeEvent: function(e) {
+        return this.fromParams({
+            key: e.which,
+            ctrlKey: e.ctrlKey,
+            _nativeEvent: e
+        });
+    }
+};
+
+});
\ No newline at end of file
index 9f58066..b2ec6ba 100644 (file)
@@ -359,32 +359,12 @@ handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE],
                 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});
                     }
@@ -399,8 +379,117 @@ handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE],
     }
 });
 
+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
index 790f56d..fbae482 100644 (file)
@@ -17,11 +17,43 @@ $.extend(CaretSelection.prototype, {
     toDocumentFragment: function() {
         var doc = this.canvas.wlxmlDocument;
         return doc.createFragment(doc.CaretFragment, {node: this.element.wlxmlNode, offset: this.offset});
+    },
+    isAtEdge: function() {
+        return this.isAtBeginning() || this.isAtEnd();
+    },
+    isAtBeginning: function() {
+        return this.offset === 0;
+    },
+    isAtEnd: function() {
+        return this.offset === this.element.getText().length;
     }
 });
 
 var TextSelection = function(canvas, params) {
+    var anchorFirst;
+
     Selection.call(this, canvas, params);
+
+    if(this.anchorElement.sameNode(this.focusElement)) {
+        anchorFirst = this.anchorOffset <= this.focusOffset;
+    } else {
+        /*jshint bitwise: false*/
+        /* globals Node */
+        anchorFirst = this.anchorElement.dom[0].compareDocumentPosition(this.focusElement.dom[0]) & Node.DOCUMENT_POSITION_FOLLOWING;
+    }
+
+    if(anchorFirst) {
+        this.startElement = this.anchorElement;
+        this.startOffset = this.anchorOffset;
+        this.endElement = this.focusElement;
+        this.endOffset = this.focusOffset;
+
+    } else {
+        this.startElement = this.focusElement;
+        this.startOffset = this.focusOffset;
+        this.endElement = this.anchorElement;
+        this.endOffset = this.anchorOffset;
+    }
 };
 TextSelection.prototype = Object.create(Selection.prototype);
 $.extend(TextSelection.prototype, {
@@ -49,6 +81,12 @@ $.extend(TextSelection.prototype, {
                 node2: siblingParents.node2
             });
         }
+    },
+    startsAtBeginning: function() {
+        return this.startOffset === 0;
+    },
+    endsAtEnd: function() {
+        return this.endOffset === this.endElement.getText().length;
     }
 });
 
index 509d743..cde08d6 100644 (file)
@@ -8,7 +8,7 @@ var _ = require('libs/underscore'),
     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');
@@ -82,6 +82,75 @@ plugin.documentExtension.textNode.transformations = {
     }
 };
 
+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,
index ed61328..99f49f4 100644 (file)
@@ -3,18 +3,29 @@ define(function(require) {
 '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 = [];
@@ -49,6 +60,15 @@ var getTextNode = function(text, doc) {
     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() {
@@ -133,5 +153,211 @@ describe('Document extensions', 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