From: Aleksander Ɓukasz Date: Thu, 24 Jul 2014 14:00:48 +0000 (+0200) Subject: editor: Improved use of contenteditable for a caret support X-Git-Url: https://git.mdrn.pl/fnpeditor.git/commitdiff_plain/2d4b18135a37d60f45c008271dbbd5c038bd76d1 editor: Improved use of contenteditable for a caret support This change stops declaring the whole canvas as being contenteditable=true, marks as such only current canvas text element instead. This approach has many advantages: 1. We no longer need to handle unspecified caret behavior, where caret could be put anywhere in the canvas DOM tree (outside of canvas text elements in particular) which needed tricky corrections on the fly. 2. Canvas widgets and custom canvas elements no longer need to use contenteditable=false nesting inside contenteditable=true, effects of which are not fully specified and which was leading to many unexpected behaviors, especially in Firefox. 3. It makes it possible the make a selection across any fragment of a document (contenteditable=true elements nested in contentedtiable=false are blocking selection crossing its borders from the inside). The downside is the need to handle keyboard navigation on the borders of canvas text elements manually. --- diff --git a/src/editor/modules/documentCanvas/canvas/canvas.html b/src/editor/modules/documentCanvas/canvas/canvas.html index e437d42..3eada65 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.html +++ b/src/editor/modules/documentCanvas/canvas/canvas.html @@ -1,5 +1,5 @@
-
+
\ No newline at end of file diff --git a/src/editor/modules/documentCanvas/canvas/canvas.js b/src/editor/modules/documentCanvas/canvas/canvas.js index d8090c8..08df11f 100644 --- a/src/editor/modules/documentCanvas/canvas/canvas.js +++ b/src/editor/modules/documentCanvas/canvas/canvas.js @@ -159,16 +159,21 @@ $.extend(Canvas.prototype, Backbone.Events, { var mouseDown; this.rootWrapper.on('mousedown', '[document-node-element], [document-text-element]', function(e) { mouseDown = e.target; + canvas.rootWrapper.find('[contenteditable]').attr('contenteditable', null); }); this.rootWrapper.on('click', '[document-node-element], [document-text-element]', function(e) { + var position; e.stopPropagation(); if(e.originalEvent.detail === 3) { e.preventDefault(); canvas._moveCaretToTextElement(canvas.getDocumentElement(e.currentTarget), 'whole'); } else { if(mouseDown === e.target) { - canvas.setCurrentElement(canvas.getDocumentElement(e.currentTarget), {caretTo: false}); + if(window.getSelection().isCollapsed) { + position = utils.caretPositionFromPoint(e.clientX, e.clientY); + canvas.setCurrentElement(canvas.getDocumentElement(position.textNode), {caretTo: position.offset}); + } } } }); @@ -409,9 +414,10 @@ $.extend(Canvas.prototype, Backbone.Events, { } var selection = document.getSelection(); + $(node).parent().attr('contenteditable', true); selection.removeAllRanges(); selection.addRange(range); - this.rootWrapper.focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't. + $(node).parent().focus(); // FF requires this for caret to be put where range colllapses, Chrome doesn't. }, setCursorPosition: function(position) { diff --git a/src/editor/modules/documentCanvas/canvas/documentElement.js b/src/editor/modules/documentCanvas/canvas/documentElement.js index 50a188a..5e653b8 100644 --- a/src/editor/modules/documentCanvas/canvas/documentElement.js +++ b/src/editor/modules/documentCanvas/canvas/documentElement.js @@ -139,8 +139,7 @@ $.extend(DocumentNodeElement.prototype, { createDOM: function() { var wrapper = $('
').attr('document-node-element', ''), widgetsContainer = $('
') - .addClass('canvas-widgets') - .attr('contenteditable', false), + .addClass('canvas-widgets'), contentContainer = $('
') .attr('document-element-content', ''); diff --git a/src/editor/modules/documentCanvas/canvas/keyboard.js b/src/editor/modules/documentCanvas/canvas/keyboard.js index fd964e0..eafd091 100644 --- a/src/editor/modules/documentCanvas/canvas/keyboard.js +++ b/src/editor/modules/documentCanvas/canvas/keyboard.js @@ -1,7 +1,8 @@ define([ +'libs/jquery', 'modules/documentCanvas/canvas/documentElement', 'modules/documentCanvas/canvas/utils' -], function(documentElement, utils) { +], function($, documentElement, utils) { 'use strict'; /* globals gettext */ @@ -36,6 +37,55 @@ var handles = function(handler, event) { return false; }; + +var scroll = function(place, textElement) { + var rect = textElement.getBoundingClientRect(), + scroll = $('#rng-module-documentCanvas-contentWrapper'), + border = rect.bottom - (place === 'top' ? rect.height : 0) - scroll.offset().top + scroll[0].scrollTop, + visible = scroll[0].scrollTop + {top: 0, bottom: scroll.height()}[place], + padding = 16, + toScroll = 0; + + if(place === 'top' && (border - padding < visible)) { + toScroll = border - visible - padding; + } else if(place === 'bottom' && (border + padding > visible)) { + toScroll = border - visible + padding; + } + if(toScroll) { + scroll[0].scrollTop = scroll[0].scrollTop + toScroll; + } + return toScroll; +}; + +var getLastRectAbove = function(node, y) { + var rects = node.getClientRects(), + idx = 0, + rect, toret; + while((rect = rects[idx])) { + if(rect.bottom < y) { + toret = rect; + } else { + break; + } + idx++; + } + return toret; +}; + +var getFirstRectBelow = function(node, y) { + var rects = node.getClientRects(), + idx = 0, + rect, toret; + while((rect = rects[idx])) { + if(rect.top > y) { + toret = rect; + break; + } + idx++; + } + return toret; +}; + var handlers = []; @@ -121,51 +171,106 @@ handlers.push({key: KEYS.ENTER, } }); -handlers.push({keys: [KEYS.ARROW_UP, KEYS.ARROW_DOWN, KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT], +handlers.push({keys: [KEYS.ARROW_UP], keydown: function(event, canvas) { - var position = canvas.getCursor().getPosition(), - element = position.element; - if(element && (element instanceof documentElement.DocumentTextElement) && element.isEmpty()) { - var direction, caretTo; - if(event.which === KEYS.ARROW_LEFT || event.which === KEYS.ARROW_UP) { - direction = 'above'; - caretTo = 'end'; - } else { - direction = 'below'; - caretTo = 'start'; - } - var el = canvas.getDocumentElement(utils.nearestInDocumentOrder('[document-text-element]', direction, element.dom[0])); - if(el) { - canvas.setCurrentElement(el, {caretTo: caretTo}); + /* 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; + } + }); + if(target) { + scrolled = scroll('top', target); + position = utils.caretPositionFromPoint(caretRect.left, rect.bottom - 1 - scrolled); + canvas.setCurrentElement(canvas.getDocumentElement(position.textNode), {caretTo: position.offset}); } } - }, - keyup: function(event, canvas) { + } +}); + +handlers.push({keys: [KEYS.ARROW_DOWN], + keydown: function(event, canvas) { + /* globals window */ var element = canvas.getCursor().getPosition().element, - caretTo = false; - if(!element) { - // Chrome hack + caretRect = window.getSelection().getRangeAt(0).getClientRects()[0], + frameRects = element.dom[0].getClientRects(), + lastRect = frameRects[frameRects.length-1], + position, target,rect, scrolled; - var moves = [{direction: 'above', caretTo: 'end'}, {direction: 'below', caretTo: 'start'}]; - if(event.which === KEYS.ARROW_RIGHT || event.which === KEYS.ARROW_DOWN) { - moves.reverse(); - } - moves.some(function(move) { - /* globals window */ - var targetNode = utils.nearestInDocumentOrder('[document-text-element]:visible', move.direction, window.getSelection().focusNode); - if(targetNode) { - element = canvas.getDocumentElement(targetNode); - caretTo = move.caretTo; - return true; // break + 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; } }); + if(target) { + scrolled = scroll('bottom', target); + position = utils.caretPositionFromPoint(caretRect.left, rect.top +1 - scrolled); + canvas.setCurrentElement(canvas.getDocumentElement(position.textNode), {caretTo: position.offset}); + } } - if(element) { - canvas.setCurrentElement(element, {caretTo: caretTo}); + } +}); + +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'}); + } } } }); +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}); + } + } 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'}); + } + } + + } +}); var selectsWholeTextElement = function(cursor) { if(cursor.isSelecting() && cursor.getSelectionStart().offsetAtBeginning && cursor.getSelectionEnd().offsetAtEnd) { diff --git a/src/editor/modules/documentCanvas/canvas/utils.js b/src/editor/modules/documentCanvas/canvas/utils.js index 058ede8..620e652 100644 --- a/src/editor/modules/documentCanvas/canvas/utils.js +++ b/src/editor/modules/documentCanvas/canvas/utils.js @@ -42,6 +42,23 @@ var getElementForDetachedNode = function(node, originalParent) { return ptr.getData('canvasElement'); }; +var caretPositionFromPoint = function(x, y) { + /* globals document */ + var range, textNode, offset; + if(document.caretPositionFromPoint) { + range = document.caretPositionFromPoint(x, y); + textNode = range.offsetNode; + offset = range.offset; + } else if(document.caretRangeFromPoint) { + range = document.caretRangeFromPoint(x, y); + textNode = range.startContainer; + offset = range.startOffset; + } + return { + textNode: textNode, + offset: offset + }; +}; return { @@ -51,7 +68,8 @@ return { }, getElementForNode: getElementForNode, getElementForDetachedNode: getElementForDetachedNode, - getElementForElementRootNode: getElementForElementRootNode + getElementForElementRootNode: getElementForElementRootNode, + caretPositionFromPoint: caretPositionFromPoint }; }); diff --git a/src/editor/modules/documentCanvas/documentCanvas.less b/src/editor/modules/documentCanvas/documentCanvas.less index 879d78d..3d335dc 100644 --- a/src/editor/modules/documentCanvas/documentCanvas.less +++ b/src/editor/modules/documentCanvas/documentCanvas.less @@ -63,6 +63,10 @@ box-shadow: 0 0 20px rgba(0, 0, 0, 0.2), inset 0 0 10px rgba(0, 0, 0, 0.1); } + [document-text-element] { + outline: none; + } + .current-text-element { } diff --git a/src/editor/plugins/core/links/box.html b/src/editor/plugins/core/links/box.html index c836ccc..08bc484 100644 --- a/src/editor/plugins/core/links/box.html +++ b/src/editor/plugins/core/links/box.html @@ -1,4 +1,4 @@ -
+