UNPKG

@testing-library/user-event

Version:
293 lines (288 loc) 12.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var isElementType = require('../misc/isElementType.js'); var isClickableInput = require('../click/isClickableInput.js'); require('../dataTransfer/Clipboard.js'); var isContentEditable = require('../edit/isContentEditable.js'); require('../edit/maxLength.js'); var isEditable = require('../edit/isEditable.js'); require('@testing-library/dom'); require('@testing-library/dom/dist/helpers.js'); var cursor = require('./cursor.js'); var resolveCaretPosition = require('./resolveCaretPosition.js'); require('../keyDef/readNextDescriptor.js'); require('../misc/level.js'); require('../../options.js'); require('../../event/eventMap.js'); require('../../event/behavior/click.js'); require('../../event/behavior/cut.js'); require('../../event/behavior/keydown.js'); require('../../event/behavior/keypress.js'); require('../../event/behavior/keyup.js'); require('../../event/behavior/paste.js'); var selection = require('../../document/selection.js'); var value = require('../../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.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.isElementType(node, 'textarea') || isElementType.isElementType(node, 'input') && node.type in isEditable.editableInputTypes); } function hasNoSelection(node) { return isElement(node) && isClickableInput.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: selection.getUISelection(element) }; } const selection$1 = 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 = isContentEditable.getContentEditable(node) && (selection$1 === null || selection$1 === void 0 ? void 0 : selection$1.anchorNode) && isContentEditable.getContentEditable(selection$1.anchorNode); return { type: isCE ? 'contenteditable' : 'default', selection: selection$1 }; } 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 = isContentEditable.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 selection.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 selection.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$1 = selection.getUISelection(node); setSelection({ focusNode: node, focusOffset: selection$1.startOffset === selection$1.endOffset ? selection$1.focusOffset + direction : direction < 0 ? selection$1.startOffset : selection$1.endOffset }); } else { const selection1 = node.ownerDocument.getSelection(); if (!(selection1 === null || selection1 === void 0 ? void 0 : selection1.focusNode)) { return; } if (selection1.isCollapsed) { const nextPosition = cursor.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 ? value.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) { selection.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.resolveCaretPosition({ target, node, offset: start }); const { node: endNode , offset: endOffset } = resolveCaretPosition.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.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; selection.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()); } } exports.getInputRange = getInputRange; exports.hasNoSelection = hasNoSelection; exports.hasOwnSelection = hasOwnSelection; exports.modifySelection = modifySelection; exports.modifySelectionPerMouseMove = modifySelectionPerMouseMove; exports.moveSelection = moveSelection; exports.setSelection = setSelection; exports.setSelectionPerMouseDown = setSelectionPerMouseDown; exports.setSelectionRange = setSelectionRange; exports.updateSelectionOnFocus = updateSelectionOnFocus;