diff --git a/src/client/core/utils/content-editable.js b/src/client/core/utils/content-editable.js index bb639ead888..43598648031 100644 --- a/src/client/core/utils/content-editable.js +++ b/src/client/core/utils/content-editable.js @@ -1,5 +1,6 @@ import * as domUtils from './dom'; import * as arrayUtils from './array'; +import { isDisplayNone } from './style'; //nodes utils @@ -257,11 +258,26 @@ export function getNearestCommonAncestor (node1, node2) { return contentEditableParent; } +function getElementVisibleChildNodes (node) { + const length = domUtils.getChildNodesLength(node.childNodes); + const result = []; + + for (let i = 0; i < length; i++) { + const child = node.childNodes[i]; + + if (!domUtils.isDomElement(child) || !isDisplayNone(child)) + result.push(child); + } + + return result; +} + //selection utils function getSelectedPositionInParentByOffset (node, offset) { var currentNode = null; var currentOffset = null; - var childCount = domUtils.getChildNodesLength(node.childNodes); + var visibleChildren = getElementVisibleChildNodes(node); + var childCount = visibleChildren.length; var isSearchForLastChild = offset >= childCount; // NOTE: we get a child element by its offset index in the parent @@ -270,9 +286,9 @@ function getSelectedPositionInParentByOffset (node, offset) { // NOTE: IE behavior if (isSearchForLastChild) - currentNode = node.childNodes[childCount - 1]; + currentNode = visibleChildren[childCount - 1]; else { - currentNode = node.childNodes[offset]; + currentNode = visibleChildren[offset]; currentOffset = 0; } @@ -284,17 +300,19 @@ function getSelectedPositionInParentByOffset (node, offset) { isSearchForLastChild = offset - 1 >= childCount; if (isSearchForLastChild) - currentNode = node.childNodes[childCount - 2]; + currentNode = visibleChildren[childCount - 2]; else { - currentNode = node.childNodes[offset - 1]; + currentNode = visibleChildren[offset - 1]; currentOffset = 0; } } // NOTE: we try to find text node while (!isSkippableNode(currentNode) && domUtils.isElementNode(currentNode)) { - if (hasChildren(currentNode)) - currentNode = currentNode.childNodes[isSearchForLastChild ? currentNode.childNodes.length - 1 : 0]; + visibleChildren = getElementVisibleChildNodes(currentNode); + + if (visibleChildren.length) + currentNode = visibleChildren[isSearchForLastChild ? visibleChildren.length - 1 : 0]; else { //NOTE: if we didn't find a text node then always set offset to zero currentOffset = 0; @@ -397,6 +415,9 @@ export function calculateNodeAndOffsetByPosition (el, offset) { } } else if (domUtils.isElementNode(target)) { + if (isDisplayNone(target)) + return point; + if (point.offset === 0 && !getContentEditableValue(target).length) { point.node = target; return point; diff --git a/src/client/core/utils/style.js b/src/client/core/utils/style.js index 7e9ca57fa89..8a200696f79 100644 --- a/src/client/core/utils/style.js +++ b/src/client/core/utils/style.js @@ -135,6 +135,10 @@ export function hasDimensions (el) { return el && !(el.offsetHeight <= 0 && el.offsetWidth <= 0); } +export function isDisplayNone (el) { + return get(el, 'display') === 'none'; +} + export function isElementHidden (el) { //NOTE: it's like jquery ':hidden' selector if (get(el, 'display') === 'none' || !hasDimensions(el) || el.type && el.type === 'hidden') @@ -144,7 +148,7 @@ export function isElementHidden (el) { var hiddenElements = []; for (var i = 0; i < elements.length; i++) { - if (get(elements[i], 'display') === 'none' || !hasDimensions(elements[i])) + if (isDisplayNone(elements[i]) || !hasDimensions(elements[i])) hiddenElements.push(elements[i]); } diff --git a/test/functional/fixtures/regression/gh-2205/pages/index.html b/test/functional/fixtures/regression/gh-2205/pages/index.html new file mode 100644 index 00000000000..f90a51ffe48 --- /dev/null +++ b/test/functional/fixtures/regression/gh-2205/pages/index.html @@ -0,0 +1,131 @@ + + + + + Title + + + + +

Has inner div with contentEditable=false

+ +
+
+
+ +

Has hidden inner div with contentEditable=false

+ +
+
+
+ +

Has inner div with contentEditable=false with focus handler

+ +
+
+
+ + + + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-2205/test.js b/test/functional/fixtures/regression/gh-2205/test.js new file mode 100644 index 00000000000..e947b52eb4e --- /dev/null +++ b/test/functional/fixtures/regression/gh-2205/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-2205)', function () { + it('Should type in div if it has an invisible child with contententeditable=false', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-2205/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-2205/testcafe-fixtures/index.js new file mode 100644 index 00000000000..1a3706553e0 --- /dev/null +++ b/test/functional/fixtures/regression/gh-2205/testcafe-fixtures/index.js @@ -0,0 +1,31 @@ +import { Selector } from 'testcafe'; + +fixture `GH-2205 - Should type in div if it has an invisible child with contententeditable=false` + .page `http://localhost:3000/fixtures/regression/gh-2205/pages/index.html`; + +test(`Click on div with placeholder`, async t => { + const editor = Selector('#editor1'); + + await t + .click(editor) + .typeText(editor, 'Hello') + .expect(editor.innerText).contains('Hello'); +}); + +test(`Click on div with always invisible placeholder`, async t => { + const editor = Selector('#editor2'); + + await t + .click(editor) + .typeText(editor, 'Hello') + .expect(editor.innerText).contains('Hello'); +}); + +test(`Click on div with outer selectionchange handler`, async t => { + const editor = Selector('#editor3'); + + await t + .click(editor) + .typeText(editor, 'Hello') + .expect(editor.innerText).contains('Hello'); +});