process.env.NODE_ENV = "production"; import preact from "preact"; import { Input } from "./Input"; import { css } from "typesafecss"; import { lazy } from "socket-function/src/caching"; import { observer } from "./observer"; import { observable } from "mobx"; // IMPORTANT! InputProps is in both InputLabel.tsx and Input.tsx, so the types export correctly export type InputProps = ( preact.JSX.HTMLAttributes & { /** ONLY throttles onChangeValue */ throttle?: number; flavor?: "large" | "small" | "none"; focusOnMount?: boolean; textarea?: boolean; /** Update on key stroke, not on blur (just does onInput = onChange, as onInput already does this) */ hot?: boolean; /** Updates arrow keys with modifier behavior to use larger numbers, instead of decimals. */ integer?: boolean; /** Only works with number/integer */ reverseArrowKeyDirection?: boolean; inputRef?: (x: HTMLInputElement | null) => void; /** Don't blur on enter key */ noEnterKeyBlur?: boolean; noFocusSelect?: boolean; inputKey?: string; fillWidth?: boolean; autocompleteValues?: string[]; /** Forces the input to update when focused. Usually we hold updates, to prevent the user's * typing to be interrupted by background updates. * NOTE: "hot" is usually required when using this. */ forceInputValueUpdatesWhenFocused?: boolean; // NOTE: We trigger onChange (and onChangeValue) whenever // e.ctrlKey && (e.code.startsWith("Key") || e.code === "Enter") || e.code === "Enter" && e.shiftKey This is because ctrl usually means a hotkey, and hotkeys usually want committed values. onChangeValue?: (value: string) => void; } ) export type InputLabelProps = Omit & { label?: preact.ComponentChild; number?: boolean; /** A number, AND, an integer. Changes behavior arrow arrow keys as well */ integer?: boolean; checkbox?: boolean; // Show text and a pencil, only showing the input on click edit?: boolean; alwaysShowPencil?: boolean; outerClass?: string; maxDecimals?: number; percent?: boolean; editClass?: string; fontSize?: number; tooltip?: string; fillWidth?: boolean; useDateUI?: boolean; }; function roundToDecimals(value: number, decimals: number) { return Math.round(value * 10 ** decimals) / 10 ** decimals; } export const startGuessDateRange = +new Date(2010, 0, 1).getTime(); export const endGuessDateRange = +new Date(2050, 0, 1).getTime(); @observer export class InputLabel extends preact.Component { synced = observable({ editting: false, editInputValue: "", editUpdateSeqNum: 0, }); render() { let props = { ...this.props }; function addValueMapping(mapper: (value: string) => string) { const baseOnChange = props.onChange; if (baseOnChange) { props.onChange = e => { baseOnChange({ currentTarget: { value: mapper(e.currentTarget.value) } } as any); }; } const baseOnChangeValue = props.onChangeValue; if (baseOnChangeValue) { props.onChangeValue = e => { baseOnChangeValue(mapper(e)); }; } const baseOnBlur = props.onBlur; if (baseOnBlur) { props.onBlur = e => { baseOnBlur({ currentTarget: { value: mapper(e.currentTarget.value) } } as any); }; } const baseOnInput = props.onInput; if (baseOnInput) { props.onInput = e => { baseOnInput({ currentTarget: { value: mapper(e.currentTarget.value) } } as any); }; } } let label = props.label || props.children; (props as any).title = props.tooltip; if ("value" in props) { props.value = props.value ?? ""; } if ((!props.type || props.type === "number") && props.useDateUI) { let value = String(props.value); props.type = "datetime-local"; props.edit = false; props.textarea = false; props.number = false; props.forceInputValueUpdatesWhenFocused = true; // NOTE: When using forceInputValueUpdatesWhenFocused we need hot, otherwise the user's updates won't be visible. props.hot = true; if (isJSNumber(value)) { value = formatDateTimeForInput(+value); } else { value = ""; } props.value = value; addValueMapping(value => (new Date(value).getTime() || Date.now()) as any); } if (props.fontSize !== undefined) { props.style = { ...props.style as any, fontSize: props.fontSize }; } if (props.integer) { props.number = true; } if (props.number) { props.type = "number"; } if (props.checkbox) { props.type = "checkbox"; } if (props.percent) { props.value = (Number(props.value) || 0) * 100; addValueMapping(value => String(+value / 100)); props.maxDecimals = props.maxDecimals ?? 2; props.number = props.number ?? true; props.type = "number"; } let maxDecimals = props.maxDecimals; if (typeof maxDecimals === "number") { props.value = roundToDecimals(Number(props.value), maxDecimals); } function formatDateTimeForInput(value: number) { value -= new Date(value).getTimezoneOffset() * 60 * 1000; return new Date(value).toISOString().slice(0, -1); } let style = { ...props.style as any }; if (props.type === "number") { let fontSize = props.fontSize ?? 12; style.width = 40 + String(props.value).length * (fontSize * 0.75); } let onClick: ((e: preact.JSX.TargetedMouseEvent) => void) | undefined; if (props.edit) { let baseBlur = props.onBlur; props.onBlur = e => { this.synced.editting = false; baseBlur?.(e); }; onClick = (e) => { e.stopPropagation(); this.synced.editting = true; }; } let stateEditting = this.synced.editting; let sizeBasedOnContents = props.edit; if (props.edit && !stateEditting) { sizeBasedOnContents = false; } if (sizeBasedOnContents) { let baseChange = props.onChange; props.onChange = e => { this.synced.editUpdateSeqNum++; baseChange?.(e); }; this.synced.editUpdateSeqNum; } let input = { if (props.edit) { this.synced.editInputValue = e.currentTarget.value; } props.onInput?.(e); }} />; if (props.edit && !stateEditting) { input = {props.value} {pencilSVG()} ; } return (