UNPKG

@testing-library/user-event

Version:
280 lines (277 loc) 12.1 kB
import { isElementType } from '../misc/isElementType.js'; import { isClickableInput } from '../click/isClickableInput.js'; import '../dataTransfer/Clipboard.js'; import { isContentEditable, getContentEditable } from '../edit/isContentEditable.js'; import '../edit/maxLength.js'; import { editableInputTypes } from '../edit/isEditable.js'; import '@testing-library/dom'; import '@testing-library/dom/dist/helpers.js'; import { getNextCursorPosition } from './cursor.js'; import { resolveCaretPosition } from './resolveCaretPosition.js'; import '../keyDef/readNextDescriptor.js'; import '../misc/level.js'; import '../../options.js'; import '../../event/eventMap.js'; import '../../event/behavior/click.js'; import '../../event/behavior/cut.js'; import '../../event/behavior/keydown.js'; import '../../event/behavior/keypress.js'; import '../../event/behavior/keyup.js'; import '../../event/behavior/paste.js'; import { setUISelection, getUISelection } from '../../document/selection.js'; import { getUIValue } from '../../document/value.js'; /** * Backward-compatible selection. * * Handles input elements and contenteditable if it only contains a single text node. */ function setSelectionRange(element, anchorOffset, focusOffset) { var ref; if (hasOwnSelection(element)) { return setSelection({ focusNode: element, anchorOffset, focusOffset }); } /* istanbul ignore else */ if (isContentEditable(element) && ((ref = element.firstChild) === null || ref === void 0 ? void 0 : ref.nodeType) === 3) { return setSelection({ focusNode: element.firstChild, anchorOffset, focusOffset }); } /* istanbul ignore next */ throw new Error('Not implemented. The result of this interaction is unreliable.'); } /** * Determine if the element has its own selection implementation * and does not interact with the Document Selection API. */ function hasOwnSelection(node) { return isElement(node) && (isElementType(node, 'textarea') || isElementType(node, 'input') && node.type in editableInputTypes); } function hasNoSelection(node) { return isElement(node) && isClickableInput(node); } function isElement(node) { return node.nodeType === 1; } /** * Determine which selection logic and selection ranges to consider. */ function getTargetTypeAndSelection(node) { const element = getElement(node); if (element && hasOwnSelection(element)) { return { type: 'input', selection: getUISelection(element) }; } const selection = element === null || element === void 0 ? void 0 : element.ownerDocument.getSelection(); // It is possible to extend a single-range selection into a contenteditable. // This results in the range acting like a range outside of contenteditable. const isCE = getContentEditable(node) && (selection === null || selection === void 0 ? void 0 : selection.anchorNode) && getContentEditable(selection.anchorNode); return { type: isCE ? 'contenteditable' : 'default', selection }; } function getElement(node) { return node.nodeType === 1 ? node : node.parentElement; } /** * Reset the Document Selection when moving focus into an element * with own selection implementation. */ function updateSelectionOnFocus(element) { const selection = element.ownerDocument.getSelection(); /* istanbul ignore if */ if (!(selection === null || selection === void 0 ? void 0 : selection.focusNode)) { return; } // If the focus moves inside an element with own selection implementation, // the document selection will be this element. // But if the focused element is inside a contenteditable, // 1) a collapsed selection will be retained. // 2) other selections will be replaced by a cursor // 2.a) at the start of the first child if it is a text node // 2.b) at the start of the contenteditable. if (hasOwnSelection(element)) { const contenteditable = getContentEditable(selection.focusNode); if (contenteditable) { if (!selection.isCollapsed) { var ref; const focusNode = ((ref = contenteditable.firstChild) === null || ref === void 0 ? void 0 : ref.nodeType) === 3 ? contenteditable.firstChild : contenteditable; selection.setBaseAndExtent(focusNode, 0, focusNode, 0); } } else { selection.setBaseAndExtent(element, 0, element, 0); } } } /** * Get the range that would be overwritten by input. */ function getInputRange(focusNode) { const typeAndSelection = getTargetTypeAndSelection(focusNode); if (typeAndSelection.type === 'input') { return typeAndSelection.selection; } else if (typeAndSelection.type === 'contenteditable') { var ref; // Multi-range on contenteditable edits the first selection instead of the last return (ref = typeAndSelection.selection) === null || ref === void 0 ? void 0 : ref.getRangeAt(0); } } /** * Extend/shrink the selection like with Shift+Arrows or Shift+Mouse */ function modifySelection({ focusNode , focusOffset }) { var ref, ref1; const typeAndSelection = getTargetTypeAndSelection(focusNode); if (typeAndSelection.type === 'input') { return setUISelection(focusNode, { anchorOffset: typeAndSelection.selection.anchorOffset, focusOffset }, 'modify'); } (ref1 = (ref = focusNode.ownerDocument) === null || ref === void 0 ? void 0 : ref.getSelection()) === null || ref1 === void 0 ? void 0 : ref1.extend(focusNode, focusOffset); } /** * Set the selection */ function setSelection({ focusNode , focusOffset , anchorNode =focusNode , anchorOffset =focusOffset }) { var ref, ref1; const typeAndSelection = getTargetTypeAndSelection(focusNode); if (typeAndSelection.type === 'input') { return setUISelection(focusNode, { anchorOffset, focusOffset }); } (ref1 = (ref = anchorNode.ownerDocument) === null || ref === void 0 ? void 0 : ref.getSelection()) === null || ref1 === void 0 ? void 0 : ref1.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); } /** * Move the selection */ function moveSelection(node, direction) { // TODO: implement shift if (hasOwnSelection(node)) { const selection = getUISelection(node); setSelection({ focusNode: node, focusOffset: selection.startOffset === selection.endOffset ? selection.focusOffset + direction : direction < 0 ? selection.startOffset : selection.endOffset }); } else { const selection1 = node.ownerDocument.getSelection(); if (!(selection1 === null || selection1 === void 0 ? void 0 : selection1.focusNode)) { return; } if (selection1.isCollapsed) { const nextPosition = getNextCursorPosition(selection1.focusNode, selection1.focusOffset, direction); if (nextPosition) { setSelection({ focusNode: nextPosition.node, focusOffset: nextPosition.offset }); } } else { selection1[direction < 0 ? 'collapseToStart' : 'collapseToEnd'](); } } } function setSelectionPerMouseDown({ document , target , clickCount , node , offset }) { if (hasNoSelection(target)) { return; } const targetHasOwnSelection = hasOwnSelection(target); // On non-input elements the text selection per multiple click // can extend beyond the target boundaries. // The exact mechanism what is considered in the same line is unclear. // Looks it might be every inline element. // TODO: Check what might be considered part of the same line of text. const text = String(targetHasOwnSelection ? getUIValue(target) : target.textContent); const [start, end] = node ? // which elements might be considered in the same line of text. // TODO: support expanding initial range on multiple clicks if node is given [ offset, offset ] : getTextRange(text, offset, clickCount); // TODO: implement modifying selection per shift/ctrl+mouse if (targetHasOwnSelection) { setUISelection(target, { anchorOffset: start !== null && start !== void 0 ? start : text.length, focusOffset: end !== null && end !== void 0 ? end : text.length }); return { node: target, start: start !== null && start !== void 0 ? start : 0, end: end !== null && end !== void 0 ? end : text.length }; } else { const { node: startNode , offset: startOffset } = resolveCaretPosition({ target, node, offset: start }); const { node: endNode , offset: endOffset } = resolveCaretPosition({ target, node, offset: end }); const range = target.ownerDocument.createRange(); try { range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); } catch (e) { throw new Error('The given offset is out of bounds.'); } const selection = document.getSelection(); selection === null || selection === void 0 ? void 0 : selection.removeAllRanges(); selection === null || selection === void 0 ? void 0 : selection.addRange(range.cloneRange()); return range; } } function getTextRange(text, pos, clickCount) { if (clickCount % 3 === 1 || text.length === 0) { return [ pos, pos ]; } const textPos = pos !== null && pos !== void 0 ? pos : text.length; if (clickCount % 3 === 2) { return [ textPos - text.substr(0, pos).match(/(\w+|\s+|\W)?$/)[0].length, pos === undefined ? pos : pos + text.substr(pos).match(/^(\w+|\s+|\W)?/)[0].length, ]; } // triple click return [ textPos - text.substr(0, pos).match(/[^\r\n]*$/)[0].length, pos === undefined ? pos : pos + text.substr(pos).match(/^[^\r\n]*/)[0].length, ]; } function modifySelectionPerMouseMove(selectionRange, { document , target , node , offset }) { const selectionFocus = resolveCaretPosition({ target, node, offset }); if ('node' in selectionRange) { // When the mouse is dragged outside of an input/textarea, // the selection is extended to the beginning or end of the input // depending on pointer position. // TODO: extend selection according to pointer position /* istanbul ignore else */ if (selectionFocus.node === selectionRange.node) { const anchorOffset = selectionFocus.offset < selectionRange.start ? selectionRange.end : selectionRange.start; const focusOffset = selectionFocus.offset > selectionRange.end || selectionFocus.offset < selectionRange.start ? selectionFocus.offset : selectionRange.end; setUISelection(selectionRange.node, { anchorOffset, focusOffset }); } } else { const range = selectionRange.cloneRange(); const cmp = range.comparePoint(selectionFocus.node, selectionFocus.offset); if (cmp < 0) { range.setStart(selectionFocus.node, selectionFocus.offset); } else if (cmp > 0) { range.setEnd(selectionFocus.node, selectionFocus.offset); } const selection = document.getSelection(); selection === null || selection === void 0 ? void 0 : selection.removeAllRanges(); selection === null || selection === void 0 ? void 0 : selection.addRange(range.cloneRange()); } } export { getInputRange, hasNoSelection, hasOwnSelection, modifySelection, modifySelectionPerMouseMove, moveSelection, setSelection, setSelectionPerMouseDown, setSelectionRange, updateSelectionOnFocus };