stupa-pdf-api/frontend/node_modules/@mui/x-date-pickers/internals/hooks/useField/useField.js

447 lines
18 KiB
JavaScript

import _extends from "@babel/runtime/helpers/esm/extends";
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
const _excluded = ["onClick", "onKeyDown", "onFocus", "onBlur", "onMouseUp", "onPaste", "error", "clearable", "onClear", "disabled"];
import * as React from 'react';
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
import useEventCallback from '@mui/utils/useEventCallback';
import useForkRef from '@mui/utils/useForkRef';
import { useTheme } from '@mui/material/styles';
import { useValidation } from '../useValidation';
import { useUtils } from '../useUtils';
import { adjustSectionValue, isAndroid, cleanString, getSectionOrder } from './useField.utils';
import { useFieldState } from './useFieldState';
import { useFieldCharacterEditing } from './useFieldCharacterEditing';
import { getActiveElement } from '../../utils/utils';
export const useField = params => {
const utils = useUtils();
const {
state,
selectedSectionIndexes,
setSelectedSections,
clearValue,
clearActiveSection,
updateSectionValue,
updateValueFromValueStr,
setTempAndroidValueStr,
sectionsValueBoundaries,
placeholder,
timezone
} = useFieldState(params);
const {
inputRef: inputRefProp,
internalProps,
internalProps: {
readOnly = false,
unstableFieldRef,
minutesStep
},
forwardedProps: {
onClick,
onKeyDown,
onFocus,
onBlur,
onMouseUp,
onPaste,
error,
clearable,
onClear,
disabled
},
fieldValueManager,
valueManager,
validator
} = params,
otherForwardedProps = _objectWithoutPropertiesLoose(params.forwardedProps, _excluded);
const {
applyCharacterEditing,
resetCharacterQuery
} = useFieldCharacterEditing({
sections: state.sections,
updateSectionValue,
sectionsValueBoundaries,
setTempAndroidValueStr,
timezone
});
const inputRef = React.useRef(null);
const handleRef = useForkRef(inputRefProp, inputRef);
const focusTimeoutRef = React.useRef(undefined);
const theme = useTheme();
const isRTL = theme.direction === 'rtl';
const sectionOrder = React.useMemo(() => getSectionOrder(state.sections, isRTL), [state.sections, isRTL]);
const syncSelectionFromDOM = () => {
var _selectionStart;
if (readOnly) {
setSelectedSections(null);
return;
}
const browserStartIndex = (_selectionStart = inputRef.current.selectionStart) != null ? _selectionStart : 0;
let nextSectionIndex;
if (browserStartIndex <= state.sections[0].startInInput) {
// Special case if browser index is in invisible characters at the beginning
nextSectionIndex = 1;
} else if (browserStartIndex >= state.sections[state.sections.length - 1].endInInput) {
// If the click is after the last character of the input, then we want to select the 1st section.
nextSectionIndex = 1;
} else {
nextSectionIndex = state.sections.findIndex(section => section.startInInput - section.startSeparator.length > browserStartIndex);
}
const sectionIndex = nextSectionIndex === -1 ? state.sections.length - 1 : nextSectionIndex - 1;
setSelectedSections(sectionIndex);
};
const handleInputClick = useEventCallback((event, ...args) => {
// The click event on the clear button would propagate to the input, trigger this handler and result in a wrong section selection.
// We avoid this by checking if the call of `handleInputClick` is actually intended, or a side effect.
if (event.isDefaultPrevented()) {
return;
}
onClick == null || onClick(event, ...args);
syncSelectionFromDOM();
});
const handleInputMouseUp = useEventCallback(event => {
onMouseUp == null || onMouseUp(event);
// Without this, the browser will remove the selected when clicking inside an already-selected section.
event.preventDefault();
});
const handleInputFocus = useEventCallback((...args) => {
onFocus == null || onFocus(...args);
// The ref is guaranteed to be resolved at this point.
const input = inputRef.current;
window.clearTimeout(focusTimeoutRef.current);
focusTimeoutRef.current = setTimeout(() => {
// The ref changed, the component got remounted, the focus event is no longer relevant.
if (!input || input !== inputRef.current) {
return;
}
if (selectedSectionIndexes != null || readOnly) {
return;
}
if (
// avoid selecting all sections when focusing empty field without value
input.value.length && Number(input.selectionEnd) - Number(input.selectionStart) === input.value.length) {
setSelectedSections('all');
} else {
syncSelectionFromDOM();
}
});
});
const handleInputBlur = useEventCallback((...args) => {
onBlur == null || onBlur(...args);
setSelectedSections(null);
});
const handleInputPaste = useEventCallback(event => {
onPaste == null || onPaste(event);
if (readOnly) {
event.preventDefault();
return;
}
const pastedValue = event.clipboardData.getData('text');
if (selectedSectionIndexes && selectedSectionIndexes.startIndex === selectedSectionIndexes.endIndex) {
const activeSection = state.sections[selectedSectionIndexes.startIndex];
const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue);
const digitsOnly = /^[0-9]+$/.test(pastedValue);
const digitsAndLetterOnly = /^(([a-zA-Z]+)|)([0-9]+)(([a-zA-Z]+)|)$/.test(pastedValue);
const isValidPastedValue = activeSection.contentType === 'letter' && lettersOnly || activeSection.contentType === 'digit' && digitsOnly || activeSection.contentType === 'digit-with-letter' && digitsAndLetterOnly;
if (isValidPastedValue) {
resetCharacterQuery();
updateSectionValue({
activeSection,
newSectionValue: pastedValue,
shouldGoToNextSection: true
});
// prevent default to avoid the input change handler being called
event.preventDefault();
return;
}
if (lettersOnly || digitsOnly) {
// The pasted value correspond to a single section but not the expected type
// skip the modification
event.preventDefault();
return;
}
}
event.preventDefault();
resetCharacterQuery();
updateValueFromValueStr(pastedValue);
});
const handleInputChange = useEventCallback(event => {
if (readOnly) {
return;
}
const targetValue = event.target.value;
if (targetValue === '') {
resetCharacterQuery();
clearValue();
return;
}
const eventData = event.nativeEvent.data;
// Calling `.fill(04/11/2022)` in playwright will trigger a change event with the requested content to insert in `event.nativeEvent.data`
// usual changes have only the currently typed character in the `event.nativeEvent.data`
const shouldUseEventData = eventData && eventData.length > 1;
const valueStr = shouldUseEventData ? eventData : targetValue;
const cleanValueStr = cleanString(valueStr);
// If no section is selected or eventData should be used, we just try to parse the new value
// This line is mostly triggered by imperative code / application tests.
if (selectedSectionIndexes == null || shouldUseEventData) {
updateValueFromValueStr(shouldUseEventData ? eventData : cleanValueStr);
return;
}
let keyPressed;
if (selectedSectionIndexes.startIndex === 0 && selectedSectionIndexes.endIndex === state.sections.length - 1 && cleanValueStr.length === 1) {
keyPressed = cleanValueStr;
} else {
const prevValueStr = cleanString(fieldValueManager.getValueStrFromSections(state.sections, isRTL));
let startOfDiffIndex = -1;
let endOfDiffIndex = -1;
for (let i = 0; i < prevValueStr.length; i += 1) {
if (startOfDiffIndex === -1 && prevValueStr[i] !== cleanValueStr[i]) {
startOfDiffIndex = i;
}
if (endOfDiffIndex === -1 && prevValueStr[prevValueStr.length - i - 1] !== cleanValueStr[cleanValueStr.length - i - 1]) {
endOfDiffIndex = i;
}
}
const activeSection = state.sections[selectedSectionIndexes.startIndex];
const hasDiffOutsideOfActiveSection = startOfDiffIndex < activeSection.start || prevValueStr.length - endOfDiffIndex - 1 > activeSection.end;
if (hasDiffOutsideOfActiveSection) {
// TODO: Support if the new date is valid
return;
}
// The active section being selected, the browser has replaced its value with the key pressed by the user.
const activeSectionEndRelativeToNewValue = cleanValueStr.length - prevValueStr.length + activeSection.end - cleanString(activeSection.endSeparator || '').length;
keyPressed = cleanValueStr.slice(activeSection.start + cleanString(activeSection.startSeparator || '').length, activeSectionEndRelativeToNewValue);
}
if (keyPressed.length === 0) {
if (isAndroid()) {
setTempAndroidValueStr(valueStr);
} else {
resetCharacterQuery();
clearActiveSection();
}
return;
}
applyCharacterEditing({
keyPressed,
sectionIndex: selectedSectionIndexes.startIndex
});
});
const handleInputKeyDown = useEventCallback(event => {
onKeyDown == null || onKeyDown(event);
// eslint-disable-next-line default-case
switch (true) {
// Select all
case event.key === 'a' && (event.ctrlKey || event.metaKey):
{
// prevent default to make sure that the next line "select all" while updating
// the internal state at the same time.
event.preventDefault();
setSelectedSections('all');
break;
}
// Move selection to next section
case event.key === 'ArrowRight':
{
event.preventDefault();
if (selectedSectionIndexes == null) {
setSelectedSections(sectionOrder.startIndex);
} else if (selectedSectionIndexes.startIndex !== selectedSectionIndexes.endIndex) {
setSelectedSections(selectedSectionIndexes.endIndex);
} else {
const nextSectionIndex = sectionOrder.neighbors[selectedSectionIndexes.startIndex].rightIndex;
if (nextSectionIndex !== null) {
setSelectedSections(nextSectionIndex);
}
}
break;
}
// Move selection to previous section
case event.key === 'ArrowLeft':
{
event.preventDefault();
if (selectedSectionIndexes == null) {
setSelectedSections(sectionOrder.endIndex);
} else if (selectedSectionIndexes.startIndex !== selectedSectionIndexes.endIndex) {
setSelectedSections(selectedSectionIndexes.startIndex);
} else {
const nextSectionIndex = sectionOrder.neighbors[selectedSectionIndexes.startIndex].leftIndex;
if (nextSectionIndex !== null) {
setSelectedSections(nextSectionIndex);
}
}
break;
}
// Reset the value of the selected section
case event.key === 'Delete':
{
event.preventDefault();
if (readOnly) {
break;
}
if (selectedSectionIndexes == null || selectedSectionIndexes.startIndex === 0 && selectedSectionIndexes.endIndex === state.sections.length - 1) {
clearValue();
} else {
clearActiveSection();
}
resetCharacterQuery();
break;
}
// Increment / decrement the selected section value
case ['ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key):
{
event.preventDefault();
if (readOnly || selectedSectionIndexes == null) {
break;
}
const activeSection = state.sections[selectedSectionIndexes.startIndex];
const activeDateManager = fieldValueManager.getActiveDateManager(utils, state, activeSection);
const newSectionValue = adjustSectionValue(utils, timezone, activeSection, event.key, sectionsValueBoundaries, activeDateManager.date, {
minutesStep
});
updateSectionValue({
activeSection,
newSectionValue,
shouldGoToNextSection: false
});
break;
}
}
});
useEnhancedEffect(() => {
if (!inputRef.current) {
return;
}
if (selectedSectionIndexes == null) {
if (inputRef.current.scrollLeft) {
// Ensure that input content is not marked as selected.
// setting selection range to 0 causes issues in Safari.
// https://bugs.webkit.org/show_bug.cgi?id=224425
inputRef.current.scrollLeft = 0;
}
return;
}
const firstSelectedSection = state.sections[selectedSectionIndexes.startIndex];
const lastSelectedSection = state.sections[selectedSectionIndexes.endIndex];
let selectionStart = firstSelectedSection.startInInput;
let selectionEnd = lastSelectedSection.endInInput;
if (selectedSectionIndexes.shouldSelectBoundarySelectors) {
selectionStart -= firstSelectedSection.startSeparator.length;
selectionEnd += lastSelectedSection.endSeparator.length;
}
if (selectionStart !== inputRef.current.selectionStart || selectionEnd !== inputRef.current.selectionEnd) {
// Fix scroll jumping on iOS browser: https://github.com/mui/mui-x/issues/8321
const currentScrollTop = inputRef.current.scrollTop;
// On multi input range pickers we want to update selection range only for the active input
// This helps to avoid the focus jumping on Safari https://github.com/mui/mui-x/issues/9003
// because WebKit implements the `setSelectionRange` based on the spec: https://bugs.webkit.org/show_bug.cgi?id=224425
if (inputRef.current === getActiveElement(document)) {
inputRef.current.setSelectionRange(selectionStart, selectionEnd);
}
// Even reading this variable seems to do the trick, but also setting it just to make use of it
inputRef.current.scrollTop = currentScrollTop;
}
});
const validationError = useValidation(_extends({}, internalProps, {
value: state.value,
timezone
}), validator, valueManager.isSameError, valueManager.defaultErrorState);
const inputError = React.useMemo(() => {
// only override when `error` is undefined.
// in case of multi input fields, the `error` value is provided externally and will always be defined.
if (error !== undefined) {
return error;
}
return valueManager.hasError(validationError);
}, [valueManager, validationError, error]);
React.useEffect(() => {
if (!inputError && !selectedSectionIndexes) {
resetCharacterQuery();
}
}, [state.referenceValue, selectedSectionIndexes, inputError]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
// Select the right section when focused on mount (`autoFocus = true` on the input)
if (inputRef.current && inputRef.current === document.activeElement) {
setSelectedSections('all');
}
return () => window.clearTimeout(focusTimeoutRef.current);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// If `state.tempValueStrAndroid` is still defined when running `useEffect`,
// Then `onChange` has only been called once, which means the user pressed `Backspace` to reset the section.
// This causes a small flickering on Android,
// But we can't use `useEnhancedEffect` which is always called before the second `onChange` call and then would cause false positives.
React.useEffect(() => {
if (state.tempValueStrAndroid != null && selectedSectionIndexes != null) {
resetCharacterQuery();
clearActiveSection();
}
}, [state.tempValueStrAndroid]); // eslint-disable-line react-hooks/exhaustive-deps
const valueStr = React.useMemo(() => {
var _state$tempValueStrAn;
return (_state$tempValueStrAn = state.tempValueStrAndroid) != null ? _state$tempValueStrAn : fieldValueManager.getValueStrFromSections(state.sections, isRTL);
}, [state.sections, fieldValueManager, state.tempValueStrAndroid, isRTL]);
const inputMode = React.useMemo(() => {
if (selectedSectionIndexes == null) {
return 'text';
}
if (state.sections[selectedSectionIndexes.startIndex].contentType === 'letter') {
return 'text';
}
return 'numeric';
}, [selectedSectionIndexes, state.sections]);
const inputHasFocus = inputRef.current && inputRef.current === getActiveElement(document);
const areAllSectionsEmpty = valueManager.areValuesEqual(utils, state.value, valueManager.emptyValue);
const shouldShowPlaceholder = !inputHasFocus && areAllSectionsEmpty;
React.useImperativeHandle(unstableFieldRef, () => ({
getSections: () => state.sections,
getActiveSectionIndex: () => {
var _selectionStart2, _selectionEnd, _inputRef$current;
const browserStartIndex = (_selectionStart2 = inputRef.current.selectionStart) != null ? _selectionStart2 : 0;
const browserEndIndex = (_selectionEnd = inputRef.current.selectionEnd) != null ? _selectionEnd : 0;
const isInputReadOnly = !!((_inputRef$current = inputRef.current) != null && _inputRef$current.readOnly);
if (browserStartIndex === 0 && browserEndIndex === 0 || isInputReadOnly) {
return null;
}
const nextSectionIndex = browserStartIndex <= state.sections[0].startInInput ? 1 // Special case if browser index is in invisible characters at the beginning.
: state.sections.findIndex(section => section.startInInput - section.startSeparator.length > browserStartIndex);
return nextSectionIndex === -1 ? state.sections.length - 1 : nextSectionIndex - 1;
},
setSelectedSections: activeSectionIndex => setSelectedSections(activeSectionIndex)
}));
const handleClearValue = useEventCallback((event, ...args) => {
var _inputRef$current2;
event.preventDefault();
onClear == null || onClear(event, ...args);
clearValue();
inputRef == null || (_inputRef$current2 = inputRef.current) == null || _inputRef$current2.focus();
setSelectedSections(0);
});
return _extends({
placeholder,
autoComplete: 'off',
disabled: Boolean(disabled)
}, otherForwardedProps, {
value: shouldShowPlaceholder ? '' : valueStr,
inputMode,
readOnly,
onClick: handleInputClick,
onFocus: handleInputFocus,
onBlur: handleInputBlur,
onPaste: handleInputPaste,
onChange: handleInputChange,
onKeyDown: handleInputKeyDown,
onMouseUp: handleInputMouseUp,
onClear: handleClearValue,
error: inputError,
ref: handleRef,
clearable: Boolean(clearable && !areAllSectionsEmpty && !readOnly && !disabled)
});
};