UNPKG

@create-figma-plugin/ui

Version:

Production-grade Preact components that replicate the Figma UI design

241 lines 10.3 kB
import { evaluateNumericExpression, isValidNumericInput, MIXED_NUMBER, MIXED_STRING } from '@create-figma-plugin/utilities'; import { h } from 'preact'; import { useCallback, useRef, useState } from 'preact/hooks'; import { createComponent } from '../../../../utilities/create-component.js'; import { getCurrentFromRef } from '../../../../utilities/get-current-from-ref.js'; import { noop } from '../../../../utilities/no-op.js'; import { computeNextValue } from '../../private/compute-next-value.js'; import { isKeyCodeCharacterGenerating } from '../../private/is-keycode-character-generating.js'; import { formatEvaluatedValue } from './format-evaluated-value.js'; const FRACTION_DIGITS = 3; const EMPTY_STRING = ''; export const RawTextboxNumeric = createComponent(function ({ disabled = false, incrementBig = 10, incrementSmall = 1, integer = false, maximum, minimum, onBlur = noop, onFocus = noop, onInput = noop, onMouseDown = noop, onKeyDown = noop, onNumericValueInput = noop, onValueInput = noop, placeholder, propagateEscapeKeyDown = true, revertOnEscapeKeyDown = false, suffix, validateOnBlur, value, ...rest }, ref) { if (typeof minimum !== 'undefined' && typeof maximum !== 'undefined' && minimum >= maximum) { throw new Error('`minimum` must be less than `maximum`'); } const inputElementRef = useRef(null); const revertOnEscapeKeyDownRef = useRef(false); const [originalValue, setOriginalValue] = useState(EMPTY_STRING); const setInputElementValue = useCallback(function (value) { const inputElement = getCurrentFromRef(inputElementRef); inputElement.value = value; const inputEvent = new window.Event('input', { bubbles: true, cancelable: true }); inputElement.dispatchEvent(inputEvent); }, []); const handleBlur = useCallback(function (event) { onBlur(event); if (revertOnEscapeKeyDownRef.current === true) { revertOnEscapeKeyDownRef.current = false; return; } if (typeof validateOnBlur !== 'undefined') { const evaluatedValue = evaluateValue(value, suffix); const result = validateOnBlur(evaluatedValue); if (typeof result === 'number') { setInputElementValue(formatEvaluatedValue(result, value, suffix)); setOriginalValue(EMPTY_STRING); return; } if (result === null) { setInputElementValue(EMPTY_STRING); setOriginalValue(EMPTY_STRING); return; } if (result === false) { if (value !== originalValue) { setInputElementValue(originalValue); } setOriginalValue(EMPTY_STRING); return; } } if (typeof suffix !== 'undefined' && value === suffix) { setInputElementValue(EMPTY_STRING); setOriginalValue(EMPTY_STRING); return; } if (value !== EMPTY_STRING && value !== MIXED_STRING) { const evaluatedValue = evaluateValue(value, suffix); const formattedValue = formatEvaluatedValue(evaluatedValue, value, suffix); if (value !== formattedValue) { setInputElementValue(formattedValue); } } setOriginalValue(EMPTY_STRING); }, [onBlur, originalValue, setInputElementValue, suffix, validateOnBlur, value]); const handleFocus = useCallback(function (event) { onFocus(event); setOriginalValue(value); event.currentTarget.select(); }, [onFocus, value]); const handleInput = useCallback(function (event) { onInput(event); const newValue = event.currentTarget.value; onValueInput(newValue); const evaluatedValue = evaluateValue(newValue, suffix); onNumericValueInput(evaluatedValue); }, [onInput, onNumericValueInput, onValueInput, suffix]); const handleKeyDown = useCallback(function (event) { onKeyDown(event); const key = event.key; if (key === 'Escape') { if (revertOnEscapeKeyDown === true) { revertOnEscapeKeyDownRef.current = true; setInputElementValue(originalValue); setOriginalValue(EMPTY_STRING); } if (propagateEscapeKeyDown === false) { event.stopPropagation(); } event.currentTarget.blur(); return; } const inputElement = event.currentTarget; if (key === 'ArrowDown' || key === 'ArrowUp') { const delta = event.shiftKey === true ? incrementBig : incrementSmall; event.preventDefault(); if (value === EMPTY_STRING || value === MIXED_STRING) { const startingValue = (function () { if (typeof minimum !== 'undefined' && minimum > 0) { return minimum; } if (typeof maximum !== 'undefined' && maximum < 0) { return maximum; } return 0; })(); const evaluatedValue = evaluateValueWithDelta(startingValue, key === 'ArrowDown' ? -1 * delta : delta); const newValue = restrictValue(evaluatedValue, minimum, maximum); const formattedValue = formatEvaluatedValue(newValue, value, suffix); inputElement.value = formattedValue; inputElement.select(); const inputEvent = new window.Event('input', { bubbles: true, cancelable: true }); inputElement.dispatchEvent(inputEvent); return; } const number = evaluateValue(value, suffix); if (number === null) { throw new Error('`number` is `null`'); } const evaluatedValue = evaluateValueWithDelta(number, key === 'ArrowDown' ? -1 * delta : delta); const newValue = restrictValue(evaluatedValue, minimum, maximum); const formattedValue = formatEvaluatedValue(newValue, value, suffix); if (formattedValue === value) { return; } inputElement.value = formattedValue; inputElement.select(); const inputEvent = new window.Event('input', { bubbles: true, cancelable: true }); inputElement.dispatchEvent(inputEvent); return; } if (event.ctrlKey === true || event.metaKey === true) { return; } if (isKeyCodeCharacterGenerating(event.keyCode) === true) { const newValue = trimSuffix(value === MIXED_STRING ? event.key : computeNextValue(inputElement, event.key), suffix); if (isValidNumericInput(newValue, { integersOnly: integer }) === false) { event.preventDefault(); return; } if (typeof minimum === 'undefined' && typeof maximum === 'undefined') { return; } const evaluatedValue = evaluateNumericExpression(newValue); if (evaluatedValue === null) { return; } if ((typeof minimum !== 'undefined' && evaluatedValue < minimum) || (typeof maximum !== 'undefined' && evaluatedValue > maximum)) { event.preventDefault(); } } }, [ incrementBig, incrementSmall, integer, maximum, minimum, onKeyDown, originalValue, propagateEscapeKeyDown, revertOnEscapeKeyDown, setInputElementValue, suffix, value ]); const handleMouseDown = useCallback(function (event) { onMouseDown(event); if (value === MIXED_STRING) { event.preventDefault(); event.currentTarget.select(); } }, [onMouseDown, value]); const handlePaste = useCallback(function (event) { if (event.clipboardData === null) { throw new Error('`event.clipboardData` is `null`'); } const nextValue = trimSuffix(computeNextValue(event.currentTarget, event.clipboardData.getData('Text')), suffix); if (isValidNumericInput(nextValue, { integersOnly: integer }) === false) { event.preventDefault(); } }, [integer, suffix]); const refCallback = useCallback(function (inputElement) { inputElementRef.current = inputElement; if (ref === null) { return; } if (typeof ref === 'function') { ref(inputElement); return; } ref.current = inputElement; }, [ref]); return (h("input", { ...rest, ref: refCallback, disabled: disabled === true, onBlur: handleBlur, onFocus: handleFocus, onInput: handleInput, onKeyDown: handleKeyDown, onMouseDown: handleMouseDown, onPaste: handlePaste, placeholder: placeholder, spellcheck: false, tabIndex: 0, type: "text", value: value === MIXED_STRING ? 'Mixed' : value })); }); function restrictValue(value, minimum, maximum) { if (typeof minimum !== 'undefined') { if (typeof maximum !== 'undefined') { return Math.min(Math.max(value, minimum), maximum); } return Math.max(value, minimum); } if (typeof maximum !== 'undefined') { return Math.min(value, maximum); } return value; } function evaluateValue(value, suffix) { if (value === MIXED_STRING) { return MIXED_NUMBER; } if (value === EMPTY_STRING) { return null; } return evaluateNumericExpression(trimSuffix(value, suffix)); } function evaluateValueWithDelta(value, delta) { return parseFloat((value + delta).toFixed(FRACTION_DIGITS)); } function trimSuffix(string, suffix) { if (typeof suffix === 'undefined') { return string; } return string.replace(new RegExp(`${suffix}$`), EMPTY_STRING); } //# sourceMappingURL=raw-textbox-numeric.js.map