'modules/documentCanvas/canvas/genericElement',
'modules/documentCanvas/canvas/nullElement',
'modules/documentCanvas/canvas/gutter',
+'modules/documentCanvas/canvas/selection',
+'modules/documentCanvas/canvas/keyEvent',
'libs/text!./canvas.html'
-], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement, nullElement, gutter, canvasTemplate) {
+], function($, _, Backbone, logging, documentElement, keyboard, utils, wlxmlListener, ElementsRegister, genericElement, nullElement, gutter, selection, keyEvent, canvasTemplate) {
'use strict';
/* global document:false, window:false, Node:false, gettext */
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;
/* globals document */
$(document.body).on('keydown', function(e) {
- var cursor = canvas.getCursor();
- if(cursor.isSelecting() || Object.keys(cursor.getPosition()).length) {
- keyboard.handleKey(e, canvas);
- }
+ canvas.triggerKeyEvent(keyEvent.fromNativeEvent(e));
});
this.rootWrapper.on('mouseup', function() {
},
triggerSelectionChanged: function() {
- this.trigger('selectionChanged', this.getSelection());
var s = this.getSelection(),
- f = s.toDocumentFragment();
+ f;
+ if(!s) {
+ return;
+ }
+ this.trigger('selectionChanged', s);
+ f = s.toDocumentFragment();
+
if(f && f instanceof f.RangeFragment) {
if(this.currentNodeElement) {
this.currentNodeElement.updateState({active: false});
},
getSelection: function() {
- return new Selection(this);
+ return selection.fromNativeSelection(this);
},
select: function(fragment) {
}
},
+ 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);
});
-var isText = function(node) {
- return node && node.nodeType === Node.TEXT_NODE && $(node.parentNode).is('[document-text-element]');
-};
-
-var Selection = function(canvas) {
- this.canvas = canvas;
- var nativeSelection = this.nativeSelection = window.getSelection();
- Object.defineProperty(this, 'type', {
- get: function() {
- if(nativeSelection.focusNode) {
- if(nativeSelection.isCollapsed && isText(nativeSelection.focusNode)) {
- return 'caret';
- }
- if(isText(nativeSelection.focusNode) && isText(nativeSelection.anchorNode)) {
- return 'textSelection';
- }
- }
- if(canvas.getCurrentNodeElement()) {
- return 'node';
- }
- }
- });
-};
-
-$.extend(Selection.prototype, {
- toDocumentFragment: function() {
- var doc = this.canvas.wlxmlDocument,
- anchorElement = this.canvas.getDocumentElement(this.nativeSelection.anchorNode),
- focusElement = this.canvas.getDocumentElement(this.nativeSelection.focusNode),
- anchorNode = anchorElement ? anchorElement.wlxmlNode : null,
- focusNode = focusElement ? focusElement.wlxmlNode : null;
- if(this.type === 'caret') {
- return doc.createFragment(doc.CaretFragment, {node: anchorNode, offset: this.nativeSelection.anchorOffset});
- }
- if(this.type === 'textSelection') {
- if(!anchorNode || !focusNode) {
- return;
- }
- if(anchorNode.isSiblingOf(focusNode)) {
- return doc.createFragment(doc.TextRangeFragment, {
- node1: anchorNode,
- offset1: this.nativeSelection.anchorOffset,
- node2: focusNode,
- offset2: this.nativeSelection.focusOffset,
- });
- }
- else {
- var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode});
- return doc.createFragment(doc.RangeFragment, {
- node1: siblingParents.node1,
- node2: siblingParents.node2
- });
- }
- }
- if(this.type === 'node') {
- return doc.createFragment(doc.NodeFragment, {node: this.canvas.getCurrentNodeElement().wlxmlNode});
- }
- },
- sameAs: function(other) {
- void(other);
- }
-});
-
var Cursor = function(canvas) {
this.canvas = canvas;
this.selection = window.getSelection();
sameNode: function(other) {
return other && (typeof other === typeof this) && other.dom[0] === this.dom[0];
},
+ isRootElement: function() {
+ return this.sameNode(this.canvas.rootElement);
+ },
trigger: function() {
this.canvas.eventBus.trigger.apply(this.canvas.eventBus, Array.prototype.slice.call(arguments, 0));
--- /dev/null
+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
define([
'libs/jquery',
-'modules/documentCanvas/canvas/documentElement',
'modules/documentCanvas/canvas/utils'
-], function($, documentElement, utils) {
+], function($, utils) {
'use strict';
/* globals gettext */
X: 88
};
-var handleKey = function(event, canvas) {
- handlers.some(function(handler) {
- if(handles(handler, event) && handler[event.type]) {
- handler[event.type](event, canvas);
- return true;
- }
- });
-};
-
-var handles = function(handler, event) {
- if(handler.key === event.which) {
- return true;
- }
- if(handler.keys && handler.keys.indexOf(event.which) !== -1) {
- return true;
- }
- return false;
-};
-
-
var scroll = function(place, textElement) {
var rect = textElement.getBoundingClientRect(),
scroll = $('#rng-module-documentCanvas-contentWrapper'),
return toret;
};
-var handlers = [];
-
-
-handlers.push({key: KEYS.ENTER,
- keydown: function(event, canvas) {
- event.preventDefault();
- var cursor = canvas.getCursor(),
- position = cursor.getPosition(),
- element = position.element;
+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 e.ctrlKey &&
+ e.key === KEYS.X &&
+ s.type === 'textSelection' &&
+ s.startsAtBeginning() &&
+ s.endsAtEnd();
+ },
+ run: function(e,s) {
+ void(s);
+ e.preventDefault();
+ }
+ },
+ {
+ applies: function(e, s) {
+ return e.key === KEYS.ARROW_UP && s.type === 'caret';
+ },
+ run: function(e, s) {
+ /* globals window */
+ var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
+ frameRects = s.element.dom[0].getClientRects(),
+ caretTop = caretRect.bottom - caretRect.height,
+ position, target,rect, scrolled;
- if(Object.keys(cursor.getPosition()).length === 0) {
- var currentElement = canvas.getCurrentNodeElement();
- if(currentElement && !currentElement.wlxmlNode.isRoot()) {
- canvas.wlxmlDocument.transaction(function() {
- var added = currentElement.wlxmlNode.after({
- tagName: currentElement.wlxmlNode.getTagName() || 'div',
- attrs: {'class': currentElement.wlxmlNode.getClass() || 'p'}
- });
- added.append({text:''});
- return added;
- }, {
- metadata: {
- description: gettext('Splitting text')
- },
- success: function(ret) {
- canvas.setCurrentElement(utils.getElementForNode(ret), {caretTo: 'start'});
+
+ if((frameRects[0].bottom === caretRect.bottom) || (caretRect.left < frameRects[0].left)) {
+ e.preventDefault();
+ s.canvas.rootWrapper.find('[document-text-element]').each(function() {
+ var test = getLastRectAbove(this, caretTop);
+ if(test) {
+ target = this;
+ rect = test;
+ } else {
+ return false;
}
});
-
+ if(target) {
+ scrolled = scroll('top', target);
+ position = utils.caretPositionFromPoint(caretRect.left, rect.bottom - 1 - scrolled);
+ s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
+ }
}
- return;
- }
-
- if(!cursor.isSelecting()) {
- if(event.ctrlKey) {
- if(element instanceof documentElement.DocumentTextElement) {
- element = element.parent();
+ if(target) {
+ scrolled = scroll('top', target);
+ var left = caretRect.left;
+ if(left > rect.left + rect.width) {
+ left = rect.left + rect.width;
+ } else if(left < rect.left ) {
+ left = rect.left;
}
-
- canvas.wlxmlDocument.transaction(function() {
- var added = element.wlxmlNode.after(
- {tagName: element.wlxmlNode.getTagName() || 'div', attrs: {'class': element.wlxmlNode.getClass() || 'p'}}
- );
- added.append({text: ''});
- return added;
- }, {
- metadata: {
- description: gettext('Splitting text')
- },
- success: function(ret) {
- canvas.setCurrentElement(utils.getElementForNode(ret), {caretTo: 'start'});
+ position = utils.caretPositionFromPoint(left, rect.bottom - 1 - scrolled);
+ s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
+ }
+ }
+ },
+ {
+ applies: function(e, s) {
+ return e.key === KEYS.ARROW_DOWN && s.type === 'caret';
+ },
+ run: function(e, s) {
+ /* globals window */
+ var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
+ frameRects = s.element.dom[0].getClientRects(),
+ lastRect = frameRects[frameRects.length-1],
+ position, target,rect, scrolled;
+
+ if(lastRect.bottom === caretRect.bottom || (caretRect.left > lastRect.left + lastRect.width)) {
+ e.preventDefault();
+ s.canvas.rootWrapper.find('[document-text-element]').each(function() {
+ var test = getFirstRectBelow(this, caretRect.bottom);
+ if(test) {
+ target = this;
+ rect = test;
+ return false;
}
});
-
- } else {
-
- if(!(element.parent().parent())) {
- return false; // top level element is unsplittable
+ if(target) {
+ scrolled = scroll('bottom', target);
+ position = utils.caretPositionFromPoint(caretRect.left, rect.top +1 - scrolled);
+ s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
}
-
- var node = position.element.wlxmlNode,
- result, goto, gotoOptions;
-
- node.document.transaction(function() {
- result = position.element.wlxmlNode.breakContent({offset: position.offset});
- }, {
- metadata: {
- description: gettext('Splitting text')
- }
- });
-
- if(result.emptyText) {
- goto = result.emptyText;
- gotoOptions = {};
- } else {
- goto = result.second;
- gotoOptions = {caretTo: 'start'};
+ }
+ if(target) {
+ scrolled = scroll('bottom', target);
+ var left = caretRect.left;
+ if(left > rect.left + rect.width) {
+ left = rect.left + rect.width;
+ } else if(left < rect.left ) {
+ left = rect.left;
}
-
- canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
+ position = utils.caretPositionFromPoint(left, rect.top +1 - scrolled);
+ s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
}
}
- }
-});
-
-handlers.push({keys: [KEYS.ARROW_UP],
- keydown: function(event, canvas) {
- /* globals window */
- var element = canvas.getCursor().getPosition().element,
- caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
- frameRects = element.dom[0].getClientRects(),
- caretTop = caretRect.bottom - caretRect.height,
- position, target,rect, scrolled;
-
-
- if((frameRects[0].bottom === caretRect.bottom) || (caretRect.left < frameRects[0].left)) {
- event.preventDefault();
- canvas.rootWrapper.find('[document-text-element]').each(function() {
- var test = getLastRectAbove(this, caretTop);
- if(test) {
- target = this;
- rect = test;
- } else {
- return false;
+ },
+ {
+ applies: function(e, s) {
+ return e.key === KEYS.ARROW_LEFT && s.type === 'caret';
+ },
+ run: function(e, s) {
+ /* globals window */
+ var prev;
+
+ if(s.offset === 0) {
+ e.preventDefault();
+ prev = s.canvas.getPreviousTextElement(s.element);
+ if(prev) {
+ scroll('top', prev.dom[0]);
+ s.canvas.setCurrentElement(s.canvas.getDocumentElement(prev.dom.contents()[0]), {caretTo: 'end'});
}
- });
- if(target) {
- scrolled = scroll('top', target);
- position = utils.caretPositionFromPoint(caretRect.left, rect.bottom - 1 - scrolled);
- canvas.setCurrentElement(canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
}
}
- }
-});
-
-handlers.push({keys: [KEYS.ARROW_DOWN],
- keydown: function(event, canvas) {
- /* globals window */
- var element = canvas.getCursor().getPosition().element,
- caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
- frameRects = element.dom[0].getClientRects(),
- lastRect = frameRects[frameRects.length-1],
- position, target,rect, scrolled;
-
- if(lastRect.bottom === caretRect.bottom || (caretRect.left > lastRect.left + lastRect.width)) {
- event.preventDefault();
- canvas.rootWrapper.find('[document-text-element]').each(function() {
- var test = getFirstRectBelow(this, caretRect.bottom);
- if(test) {
- target = this;
- rect = test;
- return false;
+ },
+ {
+ applies: function(e, s) {
+ return e.key === KEYS.ARROW_RIGHT && s.type === 'caret';
+ },
+ run: function(e, s) {
+ /* globals window */
+ var next;
+ if(s.isAtEnd()) {
+ e.preventDefault();
+ next = s.canvas.getNextTextElement(s.element);
+ if(next) {
+ scroll('bottom', next.dom[0]);
+ s.canvas.setCurrentElement(s.canvas.getDocumentElement(next.dom.contents()[0]), {caretTo: 0});
+ }
+ } else {
+ var secondToLast = (s.offset === s.element.wlxmlNode.getText().length -1);
+ if(secondToLast) {
+ // Only Flying Spaghetti Monster knows why this is need for FF (for versions at least 26 to 31)
+ e.preventDefault();
+ s.canvas.setCurrentElement(s.element, {caretTo: 'end'});
}
- });
- if(target) {
- scrolled = scroll('bottom', target);
- position = utils.caretPositionFromPoint(caretRect.left, rect.top +1 - scrolled);
- canvas.setCurrentElement(canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
}
}
- }
-});
-
-handlers.push({keys: [KEYS.ARROW_LEFT],
- keydown: function(event, canvas) {
- /* globals window */
- var position = canvas.getCursor().getPosition(),
- element = position.element,
- prev;
-
- if(position.offset === 0) {
- event.preventDefault();
- prev = canvas.getPreviousTextElement(element);
- if(prev) {
- scroll('top', prev.dom[0]);
- canvas.setCurrentElement(canvas.getDocumentElement(prev.dom.contents()[0]), {caretTo: 'end'});
- }
+ },
+ {
+ applies: function(e, s) {
+ return s.type === 'caret' &&
+ s.element.wlxmlNode.parent().is({tagName: 'span'}) &&
+ s.element.wlxmlNode.getText().length === 1 &&
+ s.offset === 1 &&
+ (e.key === KEYS.BACKSPACE);
+ },
+ run: function(e, s) {
+ var params = {},
+ prevTextNode = s.element.canvas.getPreviousTextElement(s.element).wlxmlNode;
+ e.preventDefault();
+ s.element.wlxmlNode.parent().detach(params);
+ s.canvas.setCurrentElement(
+ (params.ret && params.ret.mergedTo) || prevTextNode,
+ {caretTo: params.ret ? params.ret.previousLen : (prevTextNode ? prevTextNode.getText().length : 0)});
}
- }
-});
-
-handlers.push({keys: [KEYS.ARROW_RIGHT],
- keydown: function(event, canvas) {
- /* globals window */
- var position = canvas.getCursor().getPosition(),
- element = position.element,
- next;
- if(position.offsetAtEnd) {
- event.preventDefault();
- next = canvas.getNextTextElement(element);
- if(next) {
- scroll('bottom', next.dom[0]);
- canvas.setCurrentElement(canvas.getDocumentElement(next.dom.contents()[0]), {caretTo: 0});
+ },
+ {
+ 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 {
- var secondToLast = (position.offset === element.wlxmlNode.getText().length -1);
- if(secondToLast) {
- // Only Flying Spaghetti Monster knows why this is need for FF (for versions at least 26 to 31)
- event.preventDefault();
- canvas.setCurrentElement(element, {caretTo: 'end'});
+ else {
+ direction = 'below';
+ caretTo = 'start';
+ cursorAtOperationEdge = s.isAtEnd();
+ element = cursorAtOperationEdge && s.canvas.getNearestTextElement(direction, s.element);
}
- }
- }
-});
-
-var selectsWholeTextElement = function(cursor) {
- if(cursor.isSelecting() && cursor.getSelectionStart().offsetAtBeginning && cursor.getSelectionEnd().offsetAtEnd) {
- return true;
- }
- return false;
-};
-
-handlers.push({key: KEYS.X,
- keydown: function(event, canvas) {
- if(event.ctrlKey && selectsWholeTextElement(canvas.getCursor())) {
- event.preventDefault();
- }
- }
-});
+ if(!cursorAtOperationEdge || !element) {
+ return;
+ }
-handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE],
- keydown: function(event, canvas) {
- var cursor = canvas.getCursor(),
- position = canvas.getCursor().getPosition(),
- element = position.element,
- node = element ? element.wlxmlNode : null,
- direction = 'above',
- caretTo = 'end',
- goto;
+ e.preventDefault();
- if(!element || !node) {
- return;
+ s.canvas.wlxmlDocument.transaction(function() {
+ if(element.wlxmlNode.getIndex() === 0) {
+ goto = element.wlxmlNode.parent().moveUp();
+ } else {
+ goto = element.wlxmlNode.moveUp();
+ }
+ if(goto) {
+ s.canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
+ }
+ }, {
+ metadata: {
+ description: gettext('Remove text')
+ }
+ });
}
-
- if(event.which === KEYS.DELETE) {
- direction = 'below';
- caretTo = 'start';
+ },
+
+ {
+ applies: function(e,s) {
+ return s.type === 'caret' && s.element.getText().length === 1 && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
+ },
+ run: function(e,s) {
+ e.preventDefault();
+ s.element.wlxmlNode.setText('');
+ s.canvas.setCurrentElement(s.element, {caretTo: 0});
}
+ },
+
+ {
+ 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';
+ }
- if(cursor.isSelecting()) {
- event.preventDefault();
- var start = cursor.getSelectionStart(),
- end = cursor.getSelectionEnd();
+ e.preventDefault();
- if(direction === 'above') {
- if(start.offsetAtBeginning) {
- goto = canvas.getNearestTextElement('above', start.element);
+ if(s.startsAtBeginning && s.endsAtEnd && s.startElement.sameNode(s.endElement)) {
+ goto = s.startElement;
+ caretTo = s.startOffset;
+ } else if(direction === 'above') {
+ if(s.startsAtBeginning()) {
+ goto = s.canvas.getNearestTextElement('above', s.startElement);
caretTo = 'end';
} else {
- goto = start.element;
- caretTo = start.offset;
+ goto = s.startElement;
+ caretTo = s.startOffset;
}
} else {
- if(end.offsetAtEnd) {
- goto = canvas.getNearestTextElement('below', start.element);
+ if(s.endsAtEnd()) {
+ goto = s.canvas.getNearestTextElement('below', s.startElement);
caretTo = 'start';
} else {
- goto = end.element;
+ goto = s.endElement;
caretTo = 0;
}
}
- canvas.wlxmlDocument.deleteText({
- from: {
- node: start.element.wlxmlNode,
- offset: start.offset
- },
- to: {
- node: end.element.wlxmlNode,
- offset: end.offset
- }
- });
- if(goto) {
- canvas.setCurrentElement(goto, {caretTo: caretTo});
- }
- return;
- }
-
- var cursorAtOperationEdge = position.offsetAtBeginning;
- if(event.which === KEYS.DELETE) {
- cursorAtOperationEdge = position.offsetAtEnd;
- }
-
- var willDeleteWholeText = function() {
- return element.getText().length === 1 || selectsWholeTextElement(cursor);
- };
-
- canvas.wlxmlDocument.transaction(function() {
- if(willDeleteWholeText()) {
- 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);
+ var doc = s.canvas.wlxmlDocument;
+ doc.transaction(function() {
+
+ doc.deleteText({
+ from: {
+ node: s.startElement.wlxmlNode,
+ offset: s.startOffset
+ },
+ to: {
+ node: s.endElement.wlxmlNode,
+ offset: s.endOffset
}
- 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();
+ });
+
+ }, {
+ success: function() {
if(goto) {
- canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
+ s.canvas.setCurrentElement(goto, {caretTo: caretTo});
}
}
- event.preventDefault();
- }
- }, {
- metadata: {
- description: gettext('Remove text')
+ });
+
+ }
+ },
+ {
+ applies: function(e, s) {
+ return s.type === 'caret' && e.key === KEYS.ENTER && !s.element.parent().isRootElement();
+ },
+ run: function(e, s) {
+ var result, goto, gotoOptions;
+ void(e);
+ e.preventDefault();
+ s.canvas.wlxmlDocument.transaction(function() {
+ result = s.element.wlxmlNode.breakContent({offset: s.offset});
+ }, {
+ metadata: {
+ description: gettext('Splitting text'),
+ fragment: s.toDocumentFragment()
+ }
+ });
+
+ if(result.emptyText) {
+ goto = result.emptyText;
+ gotoOptions = {};
+ } else {
+ goto = result.second;
+ gotoOptions = {caretTo: 'start'};
}
- });
+
+ s.canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
+ }
}
-});
+];
return {
- handleKey: handleKey
+ handleKeyEvent: handleKeyEvent,
+ KEYS: KEYS
};
});
\ No newline at end of file
--- /dev/null
+define(function(require) {
+
+'use strict';
+
+var $ = require('libs/jquery');
+
+var Selection = function(canvas, params) {
+ this.canvas = canvas;
+ $.extend(this, params);
+};
+
+var CaretSelection = function(canvas, params) {
+ Selection.call(this, canvas, params);
+};
+CaretSelection.prototype = Object.create(Selection.prototype);
+$.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, {
+ toDocumentFragment: function() {
+ var doc = this.canvas.wlxmlDocument,
+ anchorNode = this.anchorElement ? this.anchorElement.wlxmlNode : null,
+ focusNode = this.focusElement ? this.focusElement.wlxmlNode : null;
+
+ if(!anchorNode || !focusNode) {
+ return;
+ }
+
+ if(anchorNode.isSiblingOf(focusNode)) {
+ return doc.createFragment(doc.TextRangeFragment, {
+ node1: anchorNode,
+ offset1: this.anchorOffset,
+ node2: focusNode,
+ offset2: this.focusOffset,
+ });
+ }
+ else {
+ var siblingParents = doc.getSiblingParents({node1: anchorNode, node2: focusNode});
+ return doc.createFragment(doc.RangeFragment, {
+ node1: siblingParents.node1,
+ node2: siblingParents.node2
+ });
+ }
+ },
+ startsAtBeginning: function() {
+ return this.startOffset === 0;
+ },
+ endsAtEnd: function() {
+ return this.endOffset === this.endElement.getText().length;
+ }
+});
+
+var NodeSelection = function(canvas, params) {
+ Selection.call(this, canvas, params);
+};
+NodeSelection.prototype = Object.create(Selection.prototype);
+$.extend(NodeSelection.prototype, {
+ toDocumentFragment: function() {
+ var doc = this.canvas.wlxmlDocument;
+ doc.createFragment(doc.NodeFragment, {node: this.element.wlxmlNode});
+ }
+});
+
+
+var isText = function(node) {
+ /* globals Node */
+ return node && node.nodeType === Node.TEXT_NODE && $(node.parentNode).is('[document-text-element]');
+};
+
+var types = {
+ caret: CaretSelection,
+ textSelection: TextSelection,
+ nodeSelection: NodeSelection
+};
+
+return {
+ fromParams: function(canvas, params) {
+ return new types[params.type](canvas, params);
+ },
+ fromNativeSelection: function(canvas) {
+ /* globals window */
+ var nativeSelection = window.getSelection(),
+ params = {},
+ element, anchorElement, focusElement;
+
+ if(nativeSelection.focusNode) {
+ if(nativeSelection.isCollapsed && isText(nativeSelection.focusNode)) {
+ element = canvas.getDocumentElement(nativeSelection.focusNode);
+ params = {
+ type: 'caret',
+ element: element,
+ offset: element.isEmpty() ? 0 : nativeSelection.focusOffset
+ };
+ } else if(isText(nativeSelection.focusNode) && isText(nativeSelection.anchorNode)) {
+ anchorElement = canvas.getDocumentElement(nativeSelection.anchorNode);
+ focusElement = canvas.getDocumentElement(nativeSelection.focusNode);
+ params = {
+ type: 'textSelection',
+ anchorElement: anchorElement,
+ anchorOffset: anchorElement.isEmpty() ? 0 : nativeSelection.anchorOffset,
+ focusElement: focusElement,
+ focusOffset: focusElement.isEmpty() ? 0 : nativeSelection.focusOffset
+ };
+ }
+ } else if((element = canvas.getCurrentNodeElement())) {
+ params = {
+ type: 'nodeSelection',
+ element: element
+ };
+ }
+ if(params.type) {
+ return this.fromParams(canvas, params);
+ }
+ }
+};
+
+});
\ 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: {}, documentNode: {}}},
Dialog = require('views/dialog/dialog'),
canvasElements = require('plugins/core/canvasElements'),
metadataEditor = require('plugins/core/metadataEditor/metadataEditor');
breakContent: {
impl: function(args) {
var node = this,
+ isSpan = node.parent().getTagName() === 'span',
parentDescribingNodes = [],
newNodes, emptyText;
newNodes = node.split({offset: args.offset});
parentDescribingNodes.forEach(function(node) {
newNodes.first.append(node);
});
+
+ var parent, newNode;
+
+ var copyNode = function(n) {
+ var attrs = {};
+ n.getAttrs().forEach(function(attr) {
+ attrs[attr.name] = attr.value;
+ });
+
+ return node.document.createDocumentNode({
+ tagName: n.getTagName(),
+ attrs: attrs
+ });
+ };
+
+ var move = function(node, to) {
+ var copy;
+ if(!node.containsNode(newNodes.second)) {
+ to.append(node);
+ return false;
+ } else {
+ if(!node.sameNode(newNodes.second)) {
+ copy = to.append(copyNode(node));
+ node.contents().some(function(n) {
+ return move(n, copy);
+ });
+ }
+ return true;
+ }
+ };
+
+ if(isSpan) {
+ newNodes.first.parents().some(function(p) {
+ if(p.getTagName() !== 'span') {
+ parent = p;
+ return true;
+ }
+ });
+ newNode = parent.before({tagName: parent.getTagName(), attrs: {'class': parent.getClass()}});
+ parent.contents().some(function(n) {
+ return move(n, newNode);
+ });
+ }
+
return _.extend(newNodes, {emptyText: emptyText});
- },
- getChangeRoot: function() {
- return this.context.parent().parent();
}
},
mergeContentUp: function() {
}
};
+plugin.documentExtension.documentNode.transformations = {
+ moveUp: function() {
+ var toMerge = this,
+ prev = toMerge.prev();
+
+ var merge = function(from, to) {
+ var toret;
+ from.contents().forEach(function(node, idx) {
+ var len, ret;
+ if(idx === 0 && node.nodeType === Node.TEXT_NODE) {
+ len = node.getText().length;
+ }
+ ret = to.append(node);
+
+ if(idx === 0 && ret.nodeType === Node.TEXT_NODE) {
+ toret = {
+ node: ret,
+ offset: ret.getText().length - len
+ };
+ } else if(!toret) {
+ toret = {
+ node: ret.getFirstTextNode(),
+ offset: 0
+ };
+ }
+ });
+ from.detach();
+ return toret;
+ };
+
+ var strategies = [
+ {
+ applies: function() {
+ return toMerge.nodeType === Node.TEXT_NODE && prev.is({tagName: 'span'});
+ },
+ run: function() {
+ var textNode = prev.getLastTextNode(),
+ txt, prevText, prevTextLen;
+ if(textNode) {
+ txt = textNode.getText();
+ if(txt.length > 1) {
+ textNode.setText(txt.substr(0, txt.length-1));
+ return {node: toMerge, offset: 0};
+ } else {
+ if((prevText = prev.prev()) && prevText.nodeType === Node.TEXT_NODE) {
+ prevTextLen = prevText.getText().length;
+ }
+ prev.detach();
+ return {
+ node: prevText ? prevText : toMerge,
+ offset : prevText ? prevTextLen : 0
+ };
+ }
+ }
+ }
+ },
+ {
+ applies: function() {
+ return toMerge.is({tagName: 'div', 'klass': 'p'}) || (toMerge.is({tagName: 'div'}) && toMerge.getClass() === '');
+ },
+ run: function() {
+ if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
+ return merge(toMerge, prev);
+ }
+ if(prev && prev.is('list')) {
+ var items = prev.contents().filter(function(n) { return n.is('item');});
+ return merge(toMerge, items[items.length-1]);
+ }
+ }
+ },
+ {
+ applies: function() {
+ return toMerge.is({tagName: 'span'});
+ },
+ run: function() {
+ /* globals Node */
+ var toret = {node: toMerge.contents()[0] , offset: 0},
+ txt, txtNode, parent;
+ if(!prev) {
+ toMerge.parents().some(function(p) {
+ if(p.is({tagName: 'span'})) {
+ parent = prev = p;
+ } else {
+ if(!parent) {
+ parent = p;
+ }
+ prev = prev && prev.prev();
+ return true;
+ }
+ });
+ }
+ if(!prev) {
+ return parent.moveUp();
+ }
+ else if(prev.nodeType === Node.TEXT_NODE && (txt = prev.getText())) {
+ prev.setText(txt.substr(0, txt.length-1));
+ return toret;
+ } else if(prev.is({tagName: 'span'})) {
+ if((txtNode = prev.getLastTextNode())) {
+ txt = txtNode.getText();
+ txtNode.setText(txt.substr(0, txt.length-1));
+ return toret;
+ }
+ }
+
+ }
+ },
+ {
+ applies: function() {
+ return toMerge.is({tagName: 'header'});
+ },
+ run: function() {
+ if(prev && prev.is('p') || prev.is({tagName: 'header'})) {
+ return merge(toMerge, prev);
+ }
+ }
+ },
+ {
+ applies: function() {
+ return toMerge.is('item');
+ },
+ run: function() {
+ var list;
+ if(prev && prev.is('item')) {
+ return 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,
define(function(require) {
'use strict';
-/* globals describe, it */
-var chai = require('libs/chai'),
+/* globals describe, it, afterEach */
+
+var $ = require('libs/jquery'),
+ _ = 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) {
+ var c = canvas.fromXMLDocument(getDocumentFromXML(xml), elements),
+ view = c.view();
+ view.attr('canvas-test', true);
+ /* globals document */
+ $(document.body).append(view);
+ return c;
+};
+var removeCanvas = function() {
+ $('[canvas-test]').remove();
+};
+
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('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() {
+ afterEach(removeCanvas);
+
+ [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');
+
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.getText()).to.equal('Ae');
+ expect(selection.offset).to.equal(1);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.wlxmlNode.getText()).to.equal(key === K.BACKSPACE ? 'A' : '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('');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.wlxmlNode.parent().sameNode(c.wlxmlDocument.root.contents()[0]));
+ });
+ });
+
+ });
+
+
+ describe('backspace at the beginning of a block', function() {
+ afterEach(removeCanvas);
+
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
+ expect(selection.offset).to.equal(1);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
+ expect(selection.offset).to.equal(1);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
+ expect(selection.offset).to.equal(1);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('AB', c))).to.equal(true);
+ expect(selection.offset).to.equal(1);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('itemparagraph', c))).to.equal(true);
+ expect(selection.offset).to.equal(4);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('item1item2', c))).to.equal(true);
+ expect(selection.offset).to.equal(5);
+ });
+ 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);
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('item1', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ 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');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('item', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ });
+
+ describe('backspace at the beginning of a span', function() {
+ afterEach(removeCanvas);
+
+ it('deletes span if it contains only one character', function() {
+ var c = getCanvasFromXML('<section>Alice<span class="emp">h</span>a cat</section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('h|').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].getText()).to.equal('Alicea cat');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('Alicea cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(5);
+ });
+
+ it('deletes from the end of the preceding text element', function() {
+ var c = getCanvasFromXML('<section>Alice<span>has a cat</span></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|has a cat').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].getText()).to.equal('Alic');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('has a cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+
+ it('deletes from the end of the preceding text element - multiple spans', function() {
+ var c = getCanvasFromXML('<section>Alice<span><span>has a cat</span></span></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|has a cat').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].getText()).to.equal('Alic');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('has a cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+
+ it('deletes from the end of the preceding span element content', function() {
+ var c = getCanvasFromXML('<section><span>Alice</span><span>has a cat</span></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|has a cat').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].is({tagName: 'span'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('Alic');
+
+ expect(rootContents[1].contents()[0].getText()).to.equal('has a cat');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('has a cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+
+ it('deletes from the end of the preceding span element content - multiple spans', function() {
+ var c = getCanvasFromXML('<section><span>Alice</span><span><span>has a cat</span></span></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|has a cat').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].is({tagName: 'span'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('Alic');
+
+ var outerSpan = rootContents[1];
+ expect(outerSpan.is({tagName: 'span'})).to.equal(true);
+
+ var innerSpan = outerSpan.contents()[0];
+ expect(innerSpan.contents()[0].getText()).to.equal('has a cat');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('has a cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+
+ it('merges two paragrahps if span is a first content of the second paragraph', function() {
+ var c = getCanvasFromXML('<section><div class="p">para</div><div class="p"><span>Alice</span> has a cat</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|Alice').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+
+ expect(rootContents.length).to.equal(1, 'single paragraph left');
+
+ var p = rootContents[0],
+ pContents = p.contents();
+
+ expect(p.is('p')).to.equal(true);
+
+ expect(pContents.length).to.equal(3);
+ expect(pContents[0].getText()).to.equal('para');
+ expect(pContents[1].contents().length).to.equal(1);
+ expect(pContents[1].contents()[0].getText()).to.equal('Alice');
+
+ expect(pContents[2].getText()).to.equal(' has a cat');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('Alice', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ });
+
+ describe('backspace before a span', function() {
+ it('deletes from the end of a span', function() {
+ var c = getCanvasFromXML('<section><span>Alice</span>has a cat</section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|has a cat').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].is({tagName: 'span'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('Alic');
+ expect(rootContents[1].getText()).to.equal('has a cat');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('has a cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ it('deletes span if it contains only one character', function() {
+ var c = getCanvasFromXML('<section>Alice <span>h</span> a cat</section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('| a cat').press(K.BACKSPACE);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(1);
+ expect(rootContents[0].getText()).to.equal('Alice a cat');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('Alice a cat', c))).to.equal(true);
+ expect(selection.offset).to.equal(6);
+ });
+ });
+
+ describe('splitting with enter', function() {
+ afterEach(removeCanvas);
+
+ it('splits paragraph into two in the middle', function() {
+ var c = getCanvasFromXML('<section><div class="p">paragraph</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('para|graph').press(K.ENTER);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('para');
+ expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[1].contents()[0].getText()).to.equal('graph');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('graph', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ it('splits paragraph into two at the beginning', function() {
+ var c = getCanvasFromXML('<section><div class="p">paragraph</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('|paragraph').press(K.ENTER);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('');
+ expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[1].contents()[0].getText()).to.equal('paragraph');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ it('splits paragraph into two at the end', function() {
+ var c = getCanvasFromXML('<section><div class="p">paragraph</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('paragraph|').press(K.ENTER);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+ expect(rootContents.length).to.equal(2);
+ expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('paragraph');
+ expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[1].contents()[0].getText()).to.equal('');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+
+ it('splits its parent box if inside a span', function() {
+ var c = getCanvasFromXML('<section><div class="p">this <span>is</span> a paragraph</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('i|s').press(K.ENTER);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+
+ expect(rootContents.length).to.equal(2);
+
+ var p1 = rootContents[0],
+ p2 = rootContents[1];
+
+ expect(p1.is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(p2.is({tagName: 'div', klass: 'p'})).to.equal(true);
+
+ var p1Contents = p1.contents(),
+ p2Contents = p2.contents();
+
+ expect(p1Contents[0].getText()).to.equal('this ');
+ expect(p1Contents[1].is({tagName: 'span'})).to.equal(true);
+ expect(p1Contents[1].contents()[0].getText()).to.equal('i');
+
+
+ expect(p2Contents[0].is({tagName: 'span'})).to.equal(true);
+ expect(p2Contents[0].contents()[0].getText()).to.equal('s');
+ expect(p2Contents[1].getText()).to.equal(' a paragraph');
+
+ var selection = c.getSelection();
+ expect(selection.element.sameNode(getTextElement('s', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+
+ it('splits its parent box if inside a double span', function() {
+ var c = getCanvasFromXML('<section><div class="p">this <span test="outer"><span test="inner">is</span></span> a paragraph</div></section>'),
+ k = new Keyboard(c);
+
+ k.withCaret('i|s').press(K.ENTER);
+
+ var rootContents = c.wlxmlDocument.root.contents();
+
+ expect(rootContents.length).to.equal(2);
+
+ var p1 = rootContents[0],
+ p2 = rootContents[1];
+
+ expect(p1.is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(p2.is({tagName: 'div', klass: 'p'})).to.equal(true);
+
+ var p1Contents = p1.contents(),
+ p2Contents = p2.contents();
+
+ /* first paragraph */
+ expect(p1Contents[0].getText()).to.equal('this ');
+
+ var outer1 = p1Contents[1];
+ expect(outer1.getAttr('test')).to.equal('outer');
+ expect(outer1.contents().length).to.equal(1);
+ var inner1 = outer1.contents()[0];
+ expect(inner1.getAttr('test')).to.equal('inner');
+ expect(inner1.contents()[0].getText()).to.equal('i');
+
+ /* second paragraph */
+ var outer2 = p2Contents[0];
+ expect(outer2.getAttr('test')).to.equal('outer');
+ expect(outer2.contents().length).to.equal(1);
+ var inner2 = outer2.contents()[0];
+ expect(inner2.getAttr('test')).to.equal('inner');
+ expect(inner2.contents()[0].getText()).to.equal('s');
+
+ expect(p2Contents[1].getText()).to.equal(' a paragraph');
+
+ /* caret */
+ var selection = c.getSelection();
+ expect(selection.element.sameNode(getTextElement('s', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ });
+
+ describe('Deleting text from a node', function() {
+ it('deletes last character with backspace', 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(2);
+ expect(rootContents[0].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[0].contents()[0].getText()).to.equal('a');
+ expect(rootContents[1].is({tagName: 'div', klass: 'p'})).to.equal(true);
+ expect(rootContents[1].contents()[0].getText()).to.equal('');
+
+ var selection = c.getSelection();
+ expect(selection.type).to.equal('caret');
+ expect(selection.element.sameNode(getTextElement('', c))).to.equal(true);
+ expect(selection.offset).to.equal(0);
+ });
+ });
+
+});
+
});
\ No newline at end of file
var elementNodeTransformations = {
detach: function(params) {
- var next;
- params = _.extend({
- normalizeStrategy: 'merge'
- }, params);
+ var next, prev;
+
+ params = params || {};
+
+ if(!params.normalizeStrategy) {
+ params.normalizeStrategy = 'merge';
+ }
if(this.parent() && this.isSurroundedByTextNodes()) {
if(params.normalizeStrategy === 'detach-left') {
this.next().detach();
} else if(params.normalizeStrategy === 'merge') {
next = this.next();
- this.prev().appendText(next.getText());
+ prev = this.prev();
+ params.ret = {
+ mergedTo: prev,
+ previousLen: prev.getText().length
+ };
+ prev.appendText(next.getText());
next.detach();
} else {
throw new Error('unknown normalize strategy for detach');
return node && (node.nativeNode === this.nativeNode || node._$.parents().index(this._$) !== -1);
},
+ getFirstTextNode: function() {
+ return this._getTextNode('first');
+ },
+
getLastTextNode: function() {
+ return this._getTextNode('last');
+ },
+
+ _getTextNode: function(which) {
var contents = this.contents(),
toret;
-
- contents.reverse().some(function(node) {
+ if(which === 'last') {
+ contents = contents.reverse();
+ }
+ contents.some(function(node) {
if(node.nodeType === Node.TEXT_NODE) {
toret = node;
return true;