From 80a3a75a7a78f0a9fb552599cd6928cc1c77918a Mon Sep 17 00:00:00 2001 From: Oliver Pulges Date: Fri, 18 Dec 2015 11:16:54 +0200 Subject: [PATCH] Fix blank row block insertions in chrome and safari --- src/commands/formatBlock.js | 40 +++++++++++++++++++-- src/dom/dom_node.js | 14 ++++---- src/selection/selection.js | 59 ++++++++++++++++++------------- test/commands/formatBlock_test.js | 12 +++++-- 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/src/commands/formatBlock.js b/src/commands/formatBlock.js index 6d23230..0ff05d6 100644 --- a/src/commands/formatBlock.js +++ b/src/commands/formatBlock.js @@ -60,7 +60,7 @@ nbIdx; for (var i = elements.length; i--;) { - if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "") { + if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) { // If cleanup removes some new block elements. remove them from newblocks array too nbIdx = wysihtml5.lang.array(newBlockElements).indexOf(elements[i]); if (nbIdx > -1) { @@ -397,8 +397,13 @@ } composer.selection.splitElementAtCaret(outerInlines.parent, fragment); } else { - // Otherwise just insert + var fc = fragment.firstChild, + lc = fragment.lastChild; + range.insertNode(fragment); + // restore range position as it might get lost in webkit sometimes + range.setStartBefore(fc); + range.setEndAfter(lc); } } } @@ -527,7 +532,21 @@ startNode = getRangeNode(r.startContainer, r.startOffset), endNode = getRangeNode(r.endContainer, r.endOffset), prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml5.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}), - nextNode = ((r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] === endNode) || (r.endContainer === endNode && endNode.nodeType === 3 && !isWhitespaceAfter(endNode, r.endOffset))) ? endNode : wysihtml5.dom.domNode(getRangeNode(r.endContainer, r.endOffset)).next({nodeTypes: [1,3], ignoreBlankTexts: true}), + nextNode = ( + ( + r.endContainer.nodeType === 1 && + r.endContainer.childNodes[r.endOffset] === endNode && + ( + endNode.nodeType === 1 || + !isWhitespaceAfter(endNode, r.endOffset) && + !wysihtml5.dom.domNode(endNode).is.rangyBookmark() + ) + ) || ( + r.endContainer === endNode && + endNode.nodeType === 3 && + !isWhitespaceAfter(endNode, r.endOffset) + ) + ) ? endNode : wysihtml5.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}), content = r.extractContents(), fragment = composer.doc.createDocumentFragment(), similarOuterBlock = similarOptions ? wysihtml5.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null, @@ -536,6 +555,11 @@ wrapper, blocks, children, firstc, lastC; + if (wysihtml5.dom.domNode(nextNode).is.rangyBookmark()) { + endNode = nextNode; + nextNode = endNode.nextSibling; + } + trimBlankTextsAndBreaks(content); if (options && options.nodeName === "BLOCKQUOTE") { @@ -585,6 +609,16 @@ } injectFragmentToRange(fragment, r, composer, firstOuterBlock); removeSurroundingLineBreaks(prevNode, nextNode, composer); + + // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block + // (if it contains rangy bookmark, so selection can be restored later correctly) + if (blocks.length > 0 && + ( + typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml5.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark() + ) + ) { + blocks[blocks.length - 1].appendChild(composer.doc.createElement('br')); + } return blocks; } diff --git a/src/dom/dom_node.js b/src/dom/dom_node.js index 95a6dc8..cb82bd5 100644 --- a/src/dom/dom_node.js +++ b/src/dom/dom_node.js @@ -14,11 +14,6 @@ return nodes; } - // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring) - function isBookmark(n) { - return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary'); - } - wysihtml5.dom.domNode = function(node) { var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE]; @@ -30,6 +25,11 @@ return node && node.nodeType === wysihtml5.TEXT_NODE && (regx).test(node.data); }, + // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring) + rangyBookmark: function() { + return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'); + }, + visible: function() { var isVisible = !(/^\s*$/g).test(wysihtml5.dom.getTextContent(node)); @@ -66,7 +66,7 @@ } if ( - isBookmark(prevNode) || // is Rangy temporary boomark element (bypass) + wysihtml5.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check. (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set ) { @@ -86,7 +86,7 @@ } if ( - isBookmark(nextNode) || // is Rangy temporary boomark element (bypass) + wysihtml5.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass) (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check. (options && options.ignoreBlankTexts && wysihtml5.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set ) { diff --git a/src/selection/selection.js b/src/selection/selection.js index 537d2be..41225a1 100644 --- a/src/selection/selection.js +++ b/src/selection/selection.js @@ -56,7 +56,7 @@ } }; - blankNode.appendChild(document.createTextNode(wysihtml5.INVISIBLE_SPACE)); + blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml5.INVISIBLE_SPACE)); blankNode.className = '_wysihtml5-temp-caret-fix'; blankNode.style.display = 'block'; blankNode.style.minWidth = '1px'; @@ -948,8 +948,8 @@ if (wysihtml5.browser.supportsSelectionModify()) { this._selectLine_W3C(); } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) { - // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)*/ - this._selectLineUniversal(); + // For IE Edge as it ditched the old api and did not fully implement the new one (as expected) + this._selectLineUniversal(); } }, @@ -965,7 +965,6 @@ } else { return node.data && node.data.length || 0; } - // body... }, anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode, fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode; @@ -1074,7 +1073,16 @@ r.moveEnd('character', 1); } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) { r.moveEnd('character', 1); - } else if (r.startOffset > 0 && ( r.startContainer.nodeType === 3 || (r.startContainer.nodeType === 1 && !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))))) { + } else if ( + r.startOffset > 0 && + ( + r.startContainer.nodeType === 3 || + ( + r.startContainer.nodeType === 1 && + !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1])) + ) + ) + ) { r.moveStart('character', -1); } } @@ -1084,6 +1092,7 @@ // Is probably just empty line as can not be expanded rect = r.nativeRange.getBoundingClientRect(); + // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes; do { amount = r.moveStart('character', -1); testRect = r.nativeRange.getBoundingClientRect(); @@ -1094,31 +1103,31 @@ } count++; } while (amount !== 0 && !found && count < 2000); - count = 0; found = false; rect = r.nativeRange.getBoundingClientRect(); - do { - amount = r.moveEnd('character', 1); - testRect = r.nativeRange.getBoundingClientRect(); - if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) { - r.moveEnd('character', -1); - - // Fix a IE line end marked by linebreak element although caret is before it - // If causes problems should be changed to be applied only to IE - if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) { - if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) { - r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length); - } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) { - r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length); + + if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) { + do { + amount = r.moveEnd('character', 1); + testRect = r.nativeRange.getBoundingClientRect(); + if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) { + r.moveEnd('character', -1); + + // Fix a IE line end marked by linebreak element although caret is before it + // If causes problems should be changed to be applied only to IE + if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) { + if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) { + r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length); + } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) { + r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length); + } } + found = true; } - - found = true; - } - count++; - } while (amount !== 0 && !found && count < 2000); - + count++; + } while (amount !== 0 && !found && count < 2000); + } r.select(); this.includeRangyRangeHelpers(); }, diff --git a/test/commands/formatBlock_test.js b/test/commands/formatBlock_test.js index 2a27347..d97b215 100644 --- a/test/commands/formatBlock_test.js +++ b/test/commands/formatBlock_test.js @@ -44,7 +44,7 @@ if (wysihtml5.browser.supported()) { // formatblock (alignment, headings, paragraph, pre, blockquote) asyncTest("Format block", function() { - expect(16); + expect(18); var that = this, parserRules = { tags: { @@ -153,12 +153,18 @@ if (wysihtml5.browser.supported()) { editor.composer.commands.remove('formatBlock'); equal(editableElement.innerHTML.toLowerCase(), 'test
foo
test', "Adding and removing block format restored initial situation"); - editor.setValue("test
foo
test", true); + editor.setValue("test
foo
test", true); that.setCaretTo(editor, editor.editableElement.childNodes[2], 1); editor.composer.commands.exec('formatBlock', "h1"); editor.composer.commands.remove('formatBlock'); equal(editableElement.innerHTML.toLowerCase(), 'test
foo
test', "Adding and removing block format restored initial situation (with caret)"); - + + editor.setValue("test
foo

", true); + that.setCaretTo(editor, editor.editableElement, 4); + editor.composer.commands.exec('formatBlock', "h1"); + equal(editableElement.innerHTML.toLowerCase(), 'test
foo


', "Adding block before last enter worked"); + editor.composer.commands.exec('insertHTML', "test"); + equal(editableElement.innerHTML.toLowerCase(), 'test
foo

test

', "... and caret is in the new empty block"); start(); });