@testing-library/user-event
Version:
Fire events the same way the user does
127 lines (123 loc) • 4.97 kB
JavaScript
var isContentEditable = require('../edit/isContentEditable.js');
var isElementType = require('../misc/isElementType.js');
function getNextCursorPosition(node, offset, direction, inputType) {
// The behavior at text node zero offset is inconsistent.
// When walking backwards:
// Firefox always moves to zero offset and jumps over last offset.
// Chrome jumps over zero offset per default but over last offset when Shift is pressed.
// The cursor always moves to zero offset if the focus area (contenteditable or body) ends there.
// When walking forward both ignore zero offset.
// When walking over input elements the cursor moves before or after that element.
// When walking over line breaks the cursor moves inside any following text node.
if (isTextNode(node) && offset + direction >= 0 && offset + direction <= node.nodeValue.length) {
return {
node,
offset: offset + direction
};
}
const nextNode = getNextCharacterContentNode(node, offset, direction);
if (nextNode) {
if (isTextNode(nextNode)) {
return {
node: nextNode,
offset: direction > 0 ? Math.min(1, nextNode.nodeValue.length) : Math.max(nextNode.nodeValue.length - 1, 0)
};
} else if (isElementType.isElementType(nextNode, 'br')) {
const nextPlusOne = getNextCharacterContentNode(nextNode, undefined, direction);
if (!nextPlusOne) {
// The behavior when there is no possible cursor position beyond the line break is inconsistent.
// In Chrome outside of contenteditable moving before a leading line break is possible.
// A leading line break can still be removed per deleteContentBackward.
// A trailing line break on the other hand is not removed by deleteContentForward.
if (direction < 0 && inputType === 'deleteContentBackward') {
return {
node: nextNode.parentNode,
offset: getOffset(nextNode)
};
}
return undefined;
} else if (isTextNode(nextPlusOne)) {
return {
node: nextPlusOne,
offset: direction > 0 ? 0 : nextPlusOne.nodeValue.length
};
} else if (direction < 0 && isElementType.isElementType(nextPlusOne, 'br')) {
return {
node: nextNode.parentNode,
offset: getOffset(nextNode)
};
} else {
return {
node: nextPlusOne.parentNode,
offset: getOffset(nextPlusOne) + (direction > 0 ? 0 : 1)
};
}
} else {
return {
node: nextNode.parentNode,
offset: getOffset(nextNode) + (direction > 0 ? 1 : 0)
};
}
}
}
function getNextCharacterContentNode(node, offset, direction) {
const nextOffset = Number(offset) + (direction < 0 ? -1 : 0);
if (offset !== undefined && isElement(node) && nextOffset >= 0 && nextOffset < node.children.length) {
node = node.children[nextOffset];
}
return walkNodes(node, direction === 1 ? 'next' : 'previous', isTreatedAsCharacterContent);
}
function isTreatedAsCharacterContent(node) {
if (isTextNode(node)) {
return true;
}
if (isElement(node)) {
if (isElementType.isElementType(node, [
'input',
'textarea'
])) {
return node.type !== 'hidden';
} else if (isElementType.isElementType(node, 'br')) {
return true;
}
}
return false;
}
function getOffset(node) {
let i = 0;
while(node.previousSibling){
i++;
node = node.previousSibling;
}
return i;
}
function isElement(node) {
return node.nodeType === 1;
}
function isTextNode(node) {
return node.nodeType === 3;
}
function walkNodes(node, direction, callback) {
for(;;){
var _node_ownerDocument;
const sibling = node[`${direction}Sibling`];
if (sibling) {
node = getDescendant(sibling, direction === 'next' ? 'first' : 'last');
if (callback(node)) {
return node;
}
} else if (node.parentNode && (!isElement(node.parentNode) || !isContentEditable.isContentEditable(node.parentNode) && node.parentNode !== ((_node_ownerDocument = node.ownerDocument) === null || _node_ownerDocument === undefined ? undefined : _node_ownerDocument.body))) {
node = node.parentNode;
} else {
break;
}
}
}
function getDescendant(node, direction) {
while(node.hasChildNodes()){
node = node[`${direction}Child`];
}
return node;
}
exports.getNextCursorPosition = getNextCursorPosition;
;