Skip to content

Commit

Permalink
[WIP]Should type in contenteditable div even if it has invisible chil…
Browse files Browse the repository at this point in the history
…d with contenteditable=false (#2205)
  • Loading branch information
VasilyStrelyaev authored and AlexKamaev committed Mar 16, 2018
1 parent ff429b2 commit 2f19d9d
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 8 deletions.
35 changes: 28 additions & 7 deletions src/client/core/utils/content-editable.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as domUtils from './dom';
import * as arrayUtils from './array';
import { isDisplayNone } from './style';


//nodes utils
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/client/core/utils/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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]);
}

Expand Down
131 changes: 131 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,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.placeholder {
color: #aaa;
font-style: italic;
height: 0;
pointer-events: none;
}

.editor.focused .placeholder {
display: 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;
}

#editor2 .placeholder {
display: none;
}
</style>
</head>
<body>

<h2>Has inner div with contentEditable=false</h2>

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

<h2>Has hidden inner div with contentEditable=false</h2>

<div class="outer-editor">
<div id="editor2" contenteditable="true" class="editor"></div>
</div>

<h2>Has inner div with contentEditable=false with focus handler</h2>

<div class="outer-editor">
<div id="editor3" contenteditable="true" class="editor"
onmousedown="event.currentTarget.className += ' focused'"></div>
</div>

<script>

function addFocusHandler () {
var stopHandling = false;
var editor = document.getElementById('editor3');

document.addEventListener('selectionchange', function () {
if (editor.className.indexOf('focused') === -1 || stopHandling)
return;

var selection = document.getSelection();
var br = document.getElementById('editor3').querySelector('p br');

if (selection.anchorNode === br) {
stopHandling = true;

var range = document.createRange();

range.setEnd(br.parentNode, 0);
range.setStart(br.parentNode, 0);
selection.removeAllRanges();
selection.addRange(range);
}
});
}

function createEditor (editor) {
var placeholder = document.createElement('div');

placeholder.textContent = 'Type here...';
placeholder.setAttribute('contenteditable', 'false');
placeholder.classList.add('placeholder');

var paragraph = document.createElement('p');
var lineBreak = document.createElement('br');

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

createEditor(document.getElementById('editor1'));
createEditor(document.getElementById('editor2'));
createEditor(document.getElementById('editor3'));

addFocusHandler();
</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');
});
});
Original file line number Diff line number Diff line change
@@ -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');
});

0 comments on commit 2f19d9d

Please sign in to comment.