UNPKG

@blueprintjs/core

Version:
317 lines 14.6 kB
/* * Copyright 2017 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var NumericInput_1; import * as tslib_1 from "tslib"; import classNames from "classnames"; import * as React from "react"; import { polyfill } from "react-lifecycles-compat"; import { AbstractPureComponent2, Classes, DISPLAYNAME_PREFIX, Keys, Position, removeNonHTMLProps, Utils, } from "../../common"; import * as Errors from "../../common/errors"; import { ButtonGroup } from "../button/buttonGroup"; import { Button } from "../button/buttons"; import { ControlGroup } from "./controlGroup"; import { InputGroup } from "./inputGroup"; import { clampValue, getValueOrEmptyValue, isFloatingPointNumericCharacter, isValidNumericKeyboardEvent, isValueNumeric, toMaxPrecision, } from "./numericInputUtils"; var IncrementDirection; (function (IncrementDirection) { IncrementDirection[IncrementDirection["DOWN"] = -1] = "DOWN"; IncrementDirection[IncrementDirection["UP"] = 1] = "UP"; })(IncrementDirection || (IncrementDirection = {})); const NON_HTML_PROPS = [ "allowNumericCharactersOnly", "buttonPosition", "clampValueOnBlur", "className", "majorStepSize", "minorStepSize", "onButtonClick", "onValueChange", "selectAllOnFocus", "selectAllOnIncrement", "stepSize", ]; let NumericInput = NumericInput_1 = class NumericInput extends AbstractPureComponent2 { constructor() { super(...arguments); this.state = { shouldSelectAfterUpdate: false, stepMaxPrecision: NumericInput_1.getStepMaxPrecision(this.props), value: getValueOrEmptyValue(this.props.value), }; // updating these flags need not trigger re-renders, so don't include them in this.state. this.didPasteEventJustOccur = false; this.delta = 0; this.inputElement = null; this.intervalId = null; this.incrementButtonHandlers = this.getButtonEventHandlers(IncrementDirection.UP); this.decrementButtonHandlers = this.getButtonEventHandlers(IncrementDirection.DOWN); this.inputRef = (input) => { this.inputElement = input; Utils.safeInvoke(this.props.inputRef, input); }; this.handleButtonClick = (e, direction) => { const delta = this.updateDelta(direction, e); const nextValue = this.incrementValue(delta); this.invokeValueCallback(nextValue, this.props.onButtonClick); }; this.stopContinuousChange = () => { this.delta = 0; this.clearTimeouts(); clearInterval(this.intervalId); document.removeEventListener("mouseup", this.stopContinuousChange); }; this.handleContinuousChange = () => { const nextValue = this.incrementValue(this.delta); this.invokeValueCallback(nextValue, this.props.onButtonClick); }; // Callbacks - Input // ================= this.handleInputFocus = (e) => { // update this state flag to trigger update for input selection (see componentDidUpdate) this.setState({ shouldSelectAfterUpdate: this.props.selectAllOnFocus }); Utils.safeInvoke(this.props.onFocus, e); }; this.handleInputBlur = (e) => { // always disable this flag on blur so it's ready for next time. this.setState({ shouldSelectAfterUpdate: false }); if (this.props.clampValueOnBlur) { const { value } = e.target; const sanitizedValue = this.getSanitizedValue(value); this.setState({ value: sanitizedValue }); } Utils.safeInvoke(this.props.onBlur, e); }; this.handleInputKeyDown = (e) => { if (this.props.disabled || this.props.readOnly) { return; } const { keyCode } = e; let direction; if (keyCode === Keys.ARROW_UP) { direction = IncrementDirection.UP; } else if (keyCode === Keys.ARROW_DOWN) { direction = IncrementDirection.DOWN; } if (direction != null) { // when the input field has focus, some key combinations will modify // the field's selection range. we'll actually want to select all // text in the field after we modify the value on the following // lines. preventing the default selection behavior lets us do that // without interference. e.preventDefault(); const delta = this.updateDelta(direction, e); this.incrementValue(delta); } Utils.safeInvoke(this.props.onKeyDown, e); }; this.handleInputKeyPress = (e) => { // we prohibit keystrokes in onKeyPress instead of onKeyDown, because // e.key is not trustworthy in onKeyDown in all browsers. if (this.props.allowNumericCharactersOnly && !isValidNumericKeyboardEvent(e)) { e.preventDefault(); } Utils.safeInvoke(this.props.onKeyPress, e); }; this.handleInputPaste = (e) => { this.didPasteEventJustOccur = true; Utils.safeInvoke(this.props.onPaste, e); }; this.handleInputChange = (e) => { const { value } = e.target; let nextValue = value; if (this.props.allowNumericCharactersOnly && this.didPasteEventJustOccur) { this.didPasteEventJustOccur = false; const valueChars = value.split(""); const sanitizedValueChars = valueChars.filter(isFloatingPointNumericCharacter); const sanitizedValue = sanitizedValueChars.join(""); nextValue = sanitizedValue; } this.setState({ shouldSelectAfterUpdate: false, value: nextValue }); }; } static getDerivedStateFromProps(props, state) { const nextState = { prevMinProp: props.min, prevMaxProp: props.max, prevValueProp: props.value }; const didMinChange = props.min !== state.prevMinProp; const didMaxChange = props.max !== state.prevMaxProp; const didBoundsChange = didMinChange || didMaxChange; const didValuePropChange = props.value !== state.prevValueProp; const value = getValueOrEmptyValue(didValuePropChange ? props.value : state.value); const sanitizedValue = value !== NumericInput_1.VALUE_EMPTY ? NumericInput_1.getSanitizedValue(value, /* delta */ 0, props.min, props.max) : NumericInput_1.VALUE_EMPTY; const stepMaxPrecision = NumericInput_1.getStepMaxPrecision(props); // if a new min and max were provided that cause the existing value to fall // outside of the new bounds, then clamp the value to the new valid range. if (didBoundsChange && sanitizedValue !== state.value) { return { ...nextState, stepMaxPrecision, value: sanitizedValue }; } else { return { ...nextState, stepMaxPrecision, value }; } } // Value Helpers // ============= static getStepMaxPrecision(props) { if (props.minorStepSize != null) { return Utils.countDecimalPlaces(props.minorStepSize); } else { return Utils.countDecimalPlaces(props.stepSize); } } static getSanitizedValue(value, stepMaxPrecision, min, max, delta = 0) { if (!isValueNumeric(value)) { return NumericInput_1.VALUE_EMPTY; } const nextValue = toMaxPrecision(parseFloat(value) + delta, stepMaxPrecision); return clampValue(nextValue, min, max).toString(); } render() { const { buttonPosition, className, fill, large } = this.props; const containerClasses = classNames(Classes.NUMERIC_INPUT, { [Classes.LARGE]: large }, className); const buttons = this.renderButtons(); return (React.createElement(ControlGroup, { className: containerClasses, fill: fill }, buttonPosition === Position.LEFT && buttons, this.renderInput(), buttonPosition === Position.RIGHT && buttons)); } componentDidUpdate(prevProps, prevState) { super.componentDidUpdate(prevProps, prevState); if (this.state.shouldSelectAfterUpdate) { this.inputElement.setSelectionRange(0, this.state.value.length); } const didControlledValueChange = this.props.value !== prevProps.value; if (!didControlledValueChange && this.state.value !== prevState.value) { this.invokeValueCallback(this.state.value, this.props.onValueChange); } } validateProps(nextProps) { const { majorStepSize, max, min, minorStepSize, stepSize } = nextProps; if (min != null && max != null && min > max) { throw new Error(Errors.NUMERIC_INPUT_MIN_MAX); } if (stepSize == null) { throw new Error(Errors.NUMERIC_INPUT_STEP_SIZE_NULL); } if (stepSize <= 0) { throw new Error(Errors.NUMERIC_INPUT_STEP_SIZE_NON_POSITIVE); } if (minorStepSize && minorStepSize <= 0) { throw new Error(Errors.NUMERIC_INPUT_MINOR_STEP_SIZE_NON_POSITIVE); } if (majorStepSize && majorStepSize <= 0) { throw new Error(Errors.NUMERIC_INPUT_MAJOR_STEP_SIZE_NON_POSITIVE); } if (minorStepSize && minorStepSize > stepSize) { throw new Error(Errors.NUMERIC_INPUT_MINOR_STEP_SIZE_BOUND); } if (majorStepSize && majorStepSize < stepSize) { throw new Error(Errors.NUMERIC_INPUT_MAJOR_STEP_SIZE_BOUND); } } // Render Helpers // ============== renderButtons() { const { intent } = this.props; const disabled = this.props.disabled || this.props.readOnly; return (React.createElement(ButtonGroup, { className: Classes.FIXED, key: "button-group", vertical: true }, React.createElement(Button, Object.assign({ disabled: disabled, icon: "chevron-up", intent: intent }, this.incrementButtonHandlers)), React.createElement(Button, Object.assign({ disabled: disabled, icon: "chevron-down", intent: intent }, this.decrementButtonHandlers)))); } renderInput() { const inputGroupHtmlProps = removeNonHTMLProps(this.props, NON_HTML_PROPS, true); return (React.createElement(InputGroup, Object.assign({ autoComplete: "off" }, inputGroupHtmlProps, { intent: this.props.intent, inputRef: this.inputRef, large: this.props.large, leftIcon: this.props.leftIcon, onFocus: this.handleInputFocus, onBlur: this.handleInputBlur, onChange: this.handleInputChange, onKeyDown: this.handleInputKeyDown, onKeyPress: this.handleInputKeyPress, onPaste: this.handleInputPaste, rightElement: this.props.rightElement, value: this.state.value }))); } // Callbacks - Buttons // =================== getButtonEventHandlers(direction) { return { // keydown is fired repeatedly when held so it's implicitly continuous onKeyDown: evt => { if (Keys.isKeyboardClick(evt.keyCode)) { this.handleButtonClick(evt, direction); } }, onMouseDown: evt => { this.handleButtonClick(evt, direction); this.startContinuousChange(); }, }; } startContinuousChange() { // The button's onMouseUp event handler doesn't fire if the user // releases outside of the button, so we need to watch all the way // from the top. document.addEventListener("mouseup", this.stopContinuousChange); // Initial delay is slightly longer to prevent the user from // accidentally triggering the continuous increment/decrement. this.setTimeout(() => { this.intervalId = window.setInterval(this.handleContinuousChange, NumericInput_1.CONTINUOUS_CHANGE_INTERVAL); }, NumericInput_1.CONTINUOUS_CHANGE_DELAY); } invokeValueCallback(value, callback) { Utils.safeInvoke(callback, +value, value); } incrementValue(delta) { // pretend we're incrementing from 0 if currValue is empty const currValue = this.state.value || NumericInput_1.VALUE_ZERO; const nextValue = this.getSanitizedValue(currValue, delta); this.setState({ shouldSelectAfterUpdate: this.props.selectAllOnIncrement, value: nextValue }); return nextValue; } getIncrementDelta(direction, isShiftKeyPressed, isAltKeyPressed) { const { majorStepSize, minorStepSize, stepSize } = this.props; if (isShiftKeyPressed && majorStepSize != null) { return direction * majorStepSize; } else if (isAltKeyPressed && minorStepSize != null) { return direction * minorStepSize; } else { return direction * stepSize; } } getSanitizedValue(value, delta = 0) { return NumericInput_1.getSanitizedValue(value, this.state.stepMaxPrecision, this.props.min, this.props.max, delta); } updateDelta(direction, e) { this.delta = this.getIncrementDelta(direction, e.shiftKey, e.altKey); return this.delta; } }; NumericInput.displayName = `${DISPLAYNAME_PREFIX}.NumericInput`; NumericInput.VALUE_EMPTY = ""; NumericInput.VALUE_ZERO = "0"; NumericInput.defaultProps = { allowNumericCharactersOnly: true, buttonPosition: Position.RIGHT, clampValueOnBlur: false, large: false, majorStepSize: 10, minorStepSize: 0.1, selectAllOnFocus: false, selectAllOnIncrement: false, stepSize: 1, value: NumericInput_1.VALUE_EMPTY, }; NumericInput.CONTINUOUS_CHANGE_DELAY = 300; NumericInput.CONTINUOUS_CHANGE_INTERVAL = 100; NumericInput = NumericInput_1 = tslib_1.__decorate([ polyfill ], NumericInput); export { NumericInput }; //# sourceMappingURL=numericInput.js.map