input.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { setUIValue, clearInitialValue, getUIValue } from '../document/UI.js';
  2. import { isElementType } from '../utils/misc/isElementType.js';
  3. import '../utils/dataTransfer/Clipboard.js';
  4. import { isValidDateOrTimeValue, buildTimeValue } from '../utils/edit/timeValue.js';
  5. import { supportsMaxLength, getMaxLength } from '../utils/edit/maxLength.js';
  6. import { getNextCursorPosition } from '../utils/focus/cursor.js';
  7. import { commitValueAfterInput } from '../document/trackValue.js';
  8. import { getInputRange } from './selection/getInputRange.js';
  9. import { setSelection } from './selection/setSelection.js';
  10. function isDateOrTime(element) {
  11. return isElementType(element, 'input') && [
  12. 'date',
  13. 'time'
  14. ].includes(element.type);
  15. }
  16. function input(instance, element, data, inputType = 'insertText') {
  17. const inputRange = getInputRange(element);
  18. /* istanbul ignore if */ if (!inputRange) {
  19. return;
  20. }
  21. // There is no `beforeinput` event on `date` and `time` input
  22. if (!isDateOrTime(element)) {
  23. const unprevented = instance.dispatchUIEvent(element, 'beforeinput', {
  24. inputType,
  25. data
  26. });
  27. if (!unprevented) {
  28. return;
  29. }
  30. }
  31. if ('startContainer' in inputRange) {
  32. editContenteditable(instance, element, inputRange, data, inputType);
  33. } else {
  34. editInputElement(instance, element, inputRange, data, inputType);
  35. }
  36. }
  37. function editContenteditable(instance, element, inputRange, data, inputType) {
  38. let del = false;
  39. if (!inputRange.collapsed) {
  40. del = true;
  41. inputRange.deleteContents();
  42. } else if ([
  43. 'deleteContentBackward',
  44. 'deleteContentForward'
  45. ].includes(inputType)) {
  46. const nextPosition = getNextCursorPosition(inputRange.startContainer, inputRange.startOffset, inputType === 'deleteContentBackward' ? -1 : 1, inputType);
  47. if (nextPosition) {
  48. del = true;
  49. const delRange = inputRange.cloneRange();
  50. if (delRange.comparePoint(nextPosition.node, nextPosition.offset) < 0) {
  51. delRange.setStart(nextPosition.node, nextPosition.offset);
  52. } else {
  53. delRange.setEnd(nextPosition.node, nextPosition.offset);
  54. }
  55. delRange.deleteContents();
  56. }
  57. }
  58. if (data) {
  59. if (inputRange.endContainer.nodeType === 3) {
  60. const offset = inputRange.endOffset;
  61. inputRange.endContainer.insertData(offset, data);
  62. inputRange.setStart(inputRange.endContainer, offset + data.length);
  63. inputRange.setEnd(inputRange.endContainer, offset + data.length);
  64. } else {
  65. const text = element.ownerDocument.createTextNode(data);
  66. inputRange.insertNode(text);
  67. inputRange.setStart(text, data.length);
  68. inputRange.setEnd(text, data.length);
  69. }
  70. }
  71. if (del || data) {
  72. instance.dispatchUIEvent(element, 'input', {
  73. inputType
  74. });
  75. }
  76. }
  77. function editInputElement(instance, element, inputRange, data, inputType) {
  78. let dataToInsert = data;
  79. if (supportsMaxLength(element)) {
  80. const maxLength = getMaxLength(element);
  81. if (maxLength !== undefined && data.length > 0) {
  82. const spaceUntilMaxLength = maxLength - element.value.length;
  83. if (spaceUntilMaxLength > 0) {
  84. dataToInsert = data.substring(0, spaceUntilMaxLength);
  85. } else {
  86. return;
  87. }
  88. }
  89. }
  90. const { newValue, newOffset, oldValue } = calculateNewValue(dataToInsert, element, inputRange, inputType);
  91. if (newValue === oldValue && newOffset === inputRange.startOffset && newOffset === inputRange.endOffset) {
  92. return;
  93. }
  94. if (isElementType(element, 'input', {
  95. type: 'number'
  96. }) && !isValidNumberInput(newValue)) {
  97. return;
  98. }
  99. setUIValue(element, newValue);
  100. setSelection({
  101. focusNode: element,
  102. anchorOffset: newOffset,
  103. focusOffset: newOffset
  104. });
  105. if (isDateOrTime(element)) {
  106. if (isValidDateOrTimeValue(element, newValue)) {
  107. commitInput(instance, element, newOffset, {});
  108. instance.dispatchUIEvent(element, 'change');
  109. clearInitialValue(element);
  110. }
  111. } else {
  112. commitInput(instance, element, newOffset, {
  113. data,
  114. inputType
  115. });
  116. }
  117. }
  118. function calculateNewValue(inputData, node, { startOffset, endOffset }, inputType) {
  119. const value = getUIValue(node);
  120. const prologEnd = Math.max(0, startOffset === endOffset && inputType === 'deleteContentBackward' ? startOffset - 1 : startOffset);
  121. const prolog = value.substring(0, prologEnd);
  122. const epilogStart = Math.min(value.length, startOffset === endOffset && inputType === 'deleteContentForward' ? startOffset + 1 : endOffset);
  123. const epilog = value.substring(epilogStart, value.length);
  124. let newValue = `${prolog}${inputData}${epilog}`;
  125. let newOffset = prologEnd + inputData.length;
  126. if (isElementType(node, 'input', {
  127. type: 'time'
  128. })) {
  129. const builtValue = buildTimeValue(newValue);
  130. if (builtValue !== '' && isValidDateOrTimeValue(node, builtValue)) {
  131. newValue = builtValue;
  132. newOffset = builtValue.length;
  133. }
  134. }
  135. return {
  136. oldValue: value,
  137. newValue,
  138. newOffset
  139. };
  140. }
  141. function commitInput(instance, element, newOffset, inputInit) {
  142. instance.dispatchUIEvent(element, 'input', inputInit);
  143. commitValueAfterInput(element, newOffset);
  144. }
  145. function isValidNumberInput(value) {
  146. var _value_match, _value_match1;
  147. // the browser allows some invalid input but not others
  148. // it allows up to two '-' at any place before any 'e' or one directly following 'e'
  149. // it allows one '.' at any place before e
  150. const valueParts = value.split('e', 2);
  151. return !(/[^\d.\-e]/.test(value) || Number((_value_match = value.match(/-/g)) === null || _value_match === undefined ? undefined : _value_match.length) > 2 || Number((_value_match1 = value.match(/\./g)) === null || _value_match1 === undefined ? undefined : _value_match1.length) > 1 || valueParts[1] && !/^-?\d*$/.test(valueParts[1]));
  152. }
  153. export { input };