Skip to content

Commit

Permalink
Should type in contenteditable div even if it has invisible child wit…
Browse files Browse the repository at this point in the history
…h contenteditable=false (DevExpress#2205) (DevExpress#2222)

* [WIP]Should type in contenteditable div even if it has invisible child with contenteditable=false(closes DevExpress#2205)

* add one more test case

* fix tests on safari osx

* fix changes after request

* fixes after review

* refactoring
  • Loading branch information
AlexKamaev authored Apr 12, 2018
1 parent 88ae1e3 commit 0f83b12
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 50 deletions.
6 changes: 4 additions & 2 deletions src/client/automation/playback/type/type-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ function _updateSelectionAfterDeletionContent (element, selection) {
return selection;
}

function _typeTextInElementNode (elementNode, text) {
function _typeTextInElementNode (elementNode, text, offset) {
var nodeForTyping = document.createTextNode(text);
var textLength = text.length;
var selectPosition = { node: nodeForTyping, offset: textLength };

if (domUtils.getTagName(elementNode) === 'br')
elementNode.parentNode.insertBefore(nodeForTyping, elementNode);
else if (offset > 0)
elementNode.insertBefore(nodeForTyping, elementNode.childNodes[offset]);
else
elementNode.appendChild(nodeForTyping);

Expand Down Expand Up @@ -126,7 +128,7 @@ function _typeTextToContentEditable (element, text) {

// NOTE: we can type only to the text nodes; for nodes with the 'element-node' type, we use a special behavior
if (domUtils.isElementNode(startNode)) {
_typeTextInElementNode(startNode, text);
_typeTextInElementNode(startNode, text, currentSelection.startPos.offset);

afterContentChanged();
return;
Expand Down
97 changes: 71 additions & 26 deletions src/client/core/utils/content-editable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as domUtils from './dom';
import * as arrayUtils from './array';

import * as styleUtils from './style';

//nodes utils
function getOwnFirstVisibleTextNode (el) {
Expand Down Expand Up @@ -34,12 +34,20 @@ function getOwnPreviousVisibleSibling (el) {
return sibling;
}

function hasChildren (node) {
return node.childNodes && domUtils.getChildNodesLength(node.childNodes);
function isVisibleNode (node) {
return domUtils.isTextNode(node) || domUtils.isElementNode(node) && styleUtils.isElementVisible(node);
}

function getVisibleChildren (node) {
return arrayUtils.filter(node.childNodes, isVisibleNode);
}

function hasVisibleChildren (node) {
return arrayUtils.some(node.childNodes, isVisibleNode);
}

function isElementWithChildren (node) {
return domUtils.isElementNode(node) || hasChildren(node);
function hasSelectableChildren (node) {
return arrayUtils.some(node.childNodes, child => isNodeSelectable(child, true));
}

//NOTE: before such elements (like div or p) adds line breaks before and after it
Expand Down Expand Up @@ -118,7 +126,7 @@ export function getFirstVisibleTextNode (el) {

if (isVisibleTextNode(curNode))
return curNode;
else if (domUtils.isRenderedNode(curNode) && isElementWithChildren(curNode) && !isNotContentEditableElement) {
else if (domUtils.isRenderedNode(curNode) && hasVisibleChildren(curNode) && !isNotContentEditableElement) {
child = getFirstVisibleTextNode(curNode);

if (child)
Expand Down Expand Up @@ -148,7 +156,7 @@ export function getLastTextNode (el, onlyVisible) {

if (visibleTextNode)
return curNode;
else if (domUtils.isRenderedNode(curNode) && isElementWithChildren(curNode) && !isNotContentEditableElement) {
else if (domUtils.isRenderedNode(curNode) && hasVisibleChildren(curNode) && !isNotContentEditableElement) {
child = getLastTextNode(curNode, false);

if (child)
Expand Down Expand Up @@ -293,8 +301,10 @@ function getSelectedPositionInParentByOffset (node, offset) {

// 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];
const visibleChildren = getVisibleChildren(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;
Expand All @@ -321,7 +331,7 @@ function getSelectionStart (el, selection, inverseSelection) {
};

//NOTE: window.getSelection() can't returns not rendered node like selected node, so we shouldn't check it
if ((domUtils.isTheSameNode(el, startNode) || domUtils.isElementNode(startNode)) && hasChildren(startNode))
if ((domUtils.isTheSameNode(el, startNode) || domUtils.isElementNode(startNode)) && hasSelectableChildren(startNode))
correctedStartPosition = getSelectedPositionInParentByOffset(startNode, startOffset);

return {
Expand All @@ -340,7 +350,7 @@ function getSelectionEnd (el, selection, inverseSelection) {
};

//NOTE: window.getSelection() can't returns not rendered node like selected node, so we shouldn't check it
if ((domUtils.isTheSameNode(el, endNode) || domUtils.isElementNode(endNode)) && hasChildren(endNode))
if ((domUtils.isTheSameNode(el, endNode) || domUtils.isElementNode(endNode)) && hasSelectableChildren(endNode))
correctedEndPosition = getSelectedPositionInParentByOffset(endNode, endOffset);

return {
Expand Down Expand Up @@ -368,6 +378,37 @@ export function getSelectionEndPosition (el, selection, inverseSelection) {
return calculatePositionByNodeAndOffset(el, correctedSelectionEnd);
}

function getElementOffset (target) {
let offset = 0;

const firstBreakElement = arrayUtils.find(target.childNodes, (node, index) => {
offset = index;
return domUtils.getTagName(node) === 'br';
});

return firstBreakElement ? offset : 0;
}

function isNodeSelectable (node, includeDescendants) {
if (styleUtils.isNotVisibleNode(node))
return false;

if (domUtils.isTextNode(node))
return true;

if (!domUtils.isElementNode(node))
return false;

if (hasSelectableChildren(node))
return includeDescendants;

const isContentEditableRoot = !domUtils.isContentEditableElement(node.parentNode);
const visibleChildren = getVisibleChildren(node);
const hasBreakLineElements = arrayUtils.some(visibleChildren, child => domUtils.getTagName(child) === 'br');

return isContentEditableRoot || hasBreakLineElements;
}

export function calculateNodeAndOffsetByPosition (el, offset) {
var point = {
node: null,
Expand Down Expand Up @@ -397,13 +438,18 @@ export function calculateNodeAndOffsetByPosition (el, offset) {
}
}
else if (domUtils.isElementNode(target)) {
if (point.offset === 0 && !getContentEditableValue(target).length) {
point.node = target;
if (!isVisibleNode(target))
return point;

if (point.offset === 0 && isNodeSelectable(target, false)) {
point.node = target;
point.offset = getElementOffset(target);

return point;
}
if (!point.node && (isNodeBlockWithBreakLine(el, target) || isNodeAfterNodeBlockWithBreakLine(el, target)))
point.offset--;
else if (!childNodesLength && domUtils.isElementNode(target) && domUtils.getTagName(target) === 'br')
else if (!childNodesLength && domUtils.getTagName(target) === 'br')
point.offset--;
}

Expand Down Expand Up @@ -499,23 +545,22 @@ export function getLastVisiblePosition (el) {
return 0;
}

//contents util
export function getContentEditableValue (target) {
var elementValue = '';
function getContentEditableNodes (target) {
var result = [];
var childNodes = target.childNodes;
var childNodesLength = domUtils.getChildNodesLength(childNodes);

if (isSkippableNode(target))
return elementValue;

if (!childNodesLength && domUtils.isTextNode(target))
return target.nodeValue;
else if (childNodesLength === 1 && domUtils.isTextNode(childNodes[0]))
return childNodes[0].nodeValue;
if (!isSkippableNode(target) && !childNodesLength && domUtils.isTextNode(target))
result.push(target);

arrayUtils.forEach(childNodes, node => {
elementValue += getContentEditableValue(node);
result = result.concat(getContentEditableNodes(node));
});

return elementValue;
return result;
}

// contents util
export function getContentEditableValue (target) {
return arrayUtils.map(getContentEditableNodes(target), node => node.nodeValue).join('');
}
25 changes: 5 additions & 20 deletions src/client/core/utils/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export var getElementPadding = hammerhead.utils.style.getElementPadding;
export var getElementScroll = hammerhead.utils.style.getElementScroll;
export var getOptionHeight = hammerhead.utils.style.getOptionHeight;
export var getSelectElementSize = hammerhead.utils.style.getSelectElementSize;
export var isElementVisible = hammerhead.utils.style.isElementVisible;
export var isSelectVisibleChild = hammerhead.utils.style.isVisibleChild;
export var getWidth = hammerhead.utils.style.getWidth;
export var getHeight = hammerhead.utils.style.getHeight;
Expand Down Expand Up @@ -40,10 +41,10 @@ var getAncestorsAndSelf = function (node) {
return getAncestors(node).concat([node]);
};

var isVisibilityHiddenTextNode = function (textNode) {
var el = domUtils.isTextNode(textNode) ? textNode.parentNode : null;
var isVisibilityHiddenNode = function (node) {
var ancestors = getAncestorsAndSelf(node);

return el && get(el, 'visibility') === 'hidden';
return some(ancestors, ancestor => domUtils.isElementNode(ancestor) && get(ancestor, 'visibility') === 'hidden');
};

var isHiddenNode = function (node) {
Expand All @@ -53,7 +54,7 @@ var isHiddenNode = function (node) {
};

export function isNotVisibleNode (node) {
return !domUtils.isRenderedNode(node) || isHiddenNode(node) || isVisibilityHiddenTextNode(node);
return !domUtils.isRenderedNode(node) || isHiddenNode(node) || isVisibilityHiddenNode(node);
}

export function getScrollableParents (element) {
Expand Down Expand Up @@ -135,22 +136,6 @@ export function hasDimensions (el) {
return el && !(el.offsetHeight <= 0 && el.offsetWidth <= 0);
}

export function isElementHidden (el) {
//NOTE: it's like jquery ':hidden' selector
if (get(el, 'display') === 'none' || !hasDimensions(el) || el.type && el.type === 'hidden')
return true;

var elements = domUtils.findDocument(el).querySelectorAll('*');
var hiddenElements = [];

for (var i = 0; i < elements.length; i++) {
if (get(elements[i], 'display') === 'none' || !hasDimensions(elements[i]))
hiddenElements.push(elements[i]);
}

return domUtils.containsElement(hiddenElements, el);
}

export function set (el, style, value) {
if (typeof style === 'string')
styleUtils.set(el, style, value);
Expand Down
10 changes: 8 additions & 2 deletions src/client/core/utils/text-selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,14 @@ export function selectByNodesAndOffsets (startPos, endPos, needFocus) {

var startNodeLength = startNode.nodeValue ? startNode.length : 0;
var endNodeLength = endNode.nodeValue ? endNode.length : 0;
var startOffset = Math.min(startNodeLength, startPos.offset);
var endOffset = Math.min(endNodeLength, endPos.offset);
var startOffset = startPos.offset;
var endOffset = endPos.offset;

if (!domUtils.isElementNode(startNode) || !startOffset)
startOffset = Math.min(startNodeLength, startPos.offset);

if (!domUtils.isElementNode(endNode) || !endOffset)
endOffset = Math.min(endNodeLength, endPos.offset);

var parentElement = contentEditable.findContentEditableParent(startNode);
var inverse = isInverseSelectionContentEditable(parentElement, startPos, endPos);
Expand Down
101 changes: 101 additions & 0 deletions test/functional/fixtures/regression/gh-2205/pages/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
#editor1.focused .placeholder {
display: none;
}

#editor2.focused .placeholder {
visibility: hidden;
}

#editor3 .placeholder {
display: none;
}

.placeholder {
color: #aaa;
font-style: italic;
height: 0;
pointer-events: none;
}

.editor {
padding: 4px 8px 4px 14px;
line-height: 1.2;
outline: none;
}

.editor {
word-wrap: break-word;
white-space: pre-wrap;
font-variant-ligatures: none;
}

.editor {
position: relative;
}

.outer-editor {
background: white;
color: black;
background-clip: padding-box;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0;
margin-bottom: 23px;
}

.outer-editor {
border: 1px solid grey;
}

.editor p:first-child {
margin-top: 10px;
}

.editor p {
margin-bottom: 1em;
}
</style>
</head>
<body>

<h2>Display: none</h2>
<div class="outer-editor">
<div id="editor1" contenteditable="true" class="editor"
onmousedown="event.currentTarget.className += ' focused'"></div>
</div>

<h2>Visibility: hidden</h2>
<div class="outer-editor">
<div id="editor2" contenteditable="true" class="editor"
onmousedown="event.currentTarget.className += ' focused'"></div>
</div>

<h2>Two hidden divs inside</h2>
<div class="outer-editor">
<div id="editor3" contenteditable="true" class="editor"><div><div class="placeholder"></div><div class="placeholder">test</div></div></div>
</div>

<div id="placeholder1" contenteditable="false" class="placeholder">Type here...</div>
<div id="placeholder2" contenteditable="false" class="placeholder">Type here...</div>

<script type="text/javascript">
function createEditor (editor, placeholder) {
var paragraph = document.createElement('p');
var lineBreak = document.createElement('br');

editor.appendChild(paragraph);
paragraph.appendChild(placeholder);
paragraph.appendChild(lineBreak);
}

createEditor(document.getElementById('editor1'), document.getElementById('placeholder1'));
createEditor(document.getElementById('editor2'), document.getElementById('placeholder2'));
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions test/functional/fixtures/regression/gh-2205/test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading

0 comments on commit 0f83b12

Please sign in to comment.