From 5ed768593857fbd22ba50bc916b09f4b621d8538 Mon Sep 17 00:00:00 2001
From: Kapu1178 <75460809+Kapu1178@users.noreply.github.com>
Date: Fri, 2 Aug 2024 13:18:18 -0400
Subject: [PATCH] update some stuffs
---
tgui/packages/tgui/components/Input.jsx | 154 --------------
tgui/packages/tgui/components/Input.tsx | 181 ++++++++++++++++
tgui/packages/tgui/components/TextArea.jsx | 173 ---------------
tgui/packages/tgui/components/TextArea.tsx | 198 ++++++++++++++++++
.../tgui/interfaces/NumberInputModal.tsx | 39 ++--
.../tgui/interfaces/TextInputModal.tsx | 62 ++++--
6 files changed, 443 insertions(+), 364 deletions(-)
delete mode 100644 tgui/packages/tgui/components/Input.jsx
create mode 100644 tgui/packages/tgui/components/Input.tsx
delete mode 100644 tgui/packages/tgui/components/TextArea.jsx
create mode 100644 tgui/packages/tgui/components/TextArea.tsx
diff --git a/tgui/packages/tgui/components/Input.jsx b/tgui/packages/tgui/components/Input.jsx
deleted file mode 100644
index f4bb8dae6433..000000000000
--- a/tgui/packages/tgui/components/Input.jsx
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-import { KEY_ENTER, KEY_ESCAPE } from 'common/keycodes';
-import { classes } from 'common/react';
-import { Component, createRef } from 'react';
-
-import { Box } from './Box';
-
-export const toInputValue = (value) =>
- typeof value !== 'number' && typeof value !== 'string' ? '' : String(value);
-
-export class Input extends Component {
- constructor(props) {
- super(props);
- this.inputRef = createRef();
- this.state = {
- editing: false,
- };
- this.handleInput = (e) => {
- const { editing } = this.state;
- const { onInput } = this.props;
- if (!editing) {
- this.setEditing(true);
- }
- if (onInput) {
- onInput(e, e.target.value);
- }
- };
- this.handleFocus = (e) => {
- const { editing } = this.state;
- if (!editing) {
- this.setEditing(true);
- }
- };
- this.handleBlur = (e) => {
- const { editing } = this.state;
- const { onChange } = this.props;
- if (editing) {
- this.setEditing(false);
- if (onChange) {
- onChange(e, e.target.value);
- }
- }
- };
- this.handleKeyDown = (e) => {
- const { onInput, onChange, onEnter } = this.props;
- if (e.keyCode === KEY_ENTER) {
- this.setEditing(false);
- if (onChange) {
- onChange(e, e.target.value);
- }
- if (onInput) {
- onInput(e, e.target.value);
- }
- if (onEnter) {
- onEnter(e, e.target.value);
- }
- if (this.props.selfClear) {
- e.target.value = '';
- } else {
- e.target.blur();
- }
- return;
- }
- if (e.keyCode === KEY_ESCAPE) {
- if (this.props.onEscape) {
- this.props.onEscape(e);
- return;
- }
-
- this.setEditing(false);
- e.target.value = toInputValue(this.props.value);
- e.target.blur();
- return;
- }
- };
- }
-
- componentDidMount() {
- const nextValue = this.props.value;
- const input = this.inputRef.current;
- if (input) {
- input.value = toInputValue(nextValue);
- }
-
- if (this.props.autoFocus || this.props.autoSelect) {
- setTimeout(() => {
- input.focus();
-
- if (this.props.autoSelect) {
- input.select();
- }
- }, 1);
- }
- }
-
- componentDidUpdate(prevProps, prevState) {
- const { editing } = this.state;
- const prevValue = prevProps.value;
- const nextValue = this.props.value;
- const input = this.inputRef.current;
- if (input && !editing && prevValue !== nextValue) {
- input.value = toInputValue(nextValue);
- }
- }
-
- setEditing(editing) {
- this.setState({ editing });
- }
-
- render() {
- const { props } = this;
- // Input only props
- const {
- selfClear,
- onInput,
- onChange,
- onEnter,
- value,
- maxLength,
- placeholder,
- ...boxProps
- } = props;
- // Box props
- const { className, fluid, monospace, ...rest } = boxProps;
- return (
-
- .
-
-
- );
- }
-}
diff --git a/tgui/packages/tgui/components/Input.tsx b/tgui/packages/tgui/components/Input.tsx
new file mode 100644
index 000000000000..9bc48aa80940
--- /dev/null
+++ b/tgui/packages/tgui/components/Input.tsx
@@ -0,0 +1,181 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { isEscape, KEY } from 'common/keys';
+import { classes } from 'common/react';
+import { debounce } from 'common/timer';
+import { KeyboardEvent, SyntheticEvent, useEffect, useRef } from 'react';
+
+import { Box, BoxProps } from './Box';
+
+type ConditionalProps =
+ | {
+ /**
+ * Mark this if you want to debounce onInput.
+ *
+ * This is useful for expensive filters, large lists etc.
+ *
+ * Requires `onInput` to be set.
+ */
+ expensive?: boolean;
+ /**
+ * Fires on each key press / value change. Used for searching.
+ *
+ * If it's a large list, consider using `expensive` prop.
+ */
+ onInput: (event: SyntheticEvent, value: string) => void;
+ }
+ | {
+ /** This prop requires onInput to be set */
+ expensive?: never;
+ onInput?: never;
+ };
+
+type OptionalProps = Partial<{
+ /** Automatically focuses the input on mount */
+ autoFocus: boolean;
+ /** Automatically selects the input value on focus */
+ autoSelect: boolean;
+ /** The class name of the input */
+ className: string;
+ /** Disables the input */
+ disabled: boolean;
+ /** Mark this if you want the input to be as wide as possible */
+ fluid: boolean;
+ /** The maximum length of the input value */
+ maxLength: number;
+ /** Mark this if you want to use a monospace font */
+ monospace: boolean;
+ /** Fires when user is 'done typing': Clicked out, blur, enter key */
+ onChange: (event: SyntheticEvent, value: string) => void;
+ /** Fires once the enter key is pressed */
+ onEnter?: (event: SyntheticEvent, value: string) => void;
+ /** Fires once the escape key is pressed */
+ onEscape: (event: SyntheticEvent) => void;
+ /** The placeholder text when everything is cleared */
+ placeholder: string;
+ /** Clears the input value on enter */
+ selfClear: boolean;
+ /** The state variable of the input. */
+ value: string | number;
+}>;
+
+type Props = OptionalProps & ConditionalProps & BoxProps;
+
+export function toInputValue(value: string | number | undefined) {
+ return typeof value !== 'number' && typeof value !== 'string'
+ ? ''
+ : String(value);
+}
+
+const inputDebounce = debounce((onInput: () => void) => onInput(), 250);
+
+/**
+ * ### Input
+ * A basic text input which allow users to enter text into a UI.
+ * > Input does not support custom font size and height due to the way
+ * > it's implemented in CSS. Eventually, this needs to be fixed.
+ */
+export function Input(props: Props) {
+ const {
+ autoFocus,
+ autoSelect,
+ className,
+ disabled,
+ expensive,
+ fluid,
+ maxLength,
+ monospace,
+ onChange,
+ onEnter,
+ onEscape,
+ onInput,
+ placeholder,
+ selfClear,
+ value,
+ ...rest
+ } = props;
+
+ // The ref to the input field
+ const inputRef = useRef(null);
+
+ function handleInput(event: SyntheticEvent) {
+ if (!onInput) return;
+
+ const value = event.currentTarget?.value;
+
+ if (expensive) {
+ inputDebounce(() => onInput(event, value));
+ } else {
+ onInput(event, value);
+ }
+ }
+
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === KEY.Enter) {
+ onEnter?.(event, event.currentTarget.value);
+ if (selfClear) {
+ event.currentTarget.value = '';
+ } else {
+ event.currentTarget.blur();
+ onChange?.(event, event.currentTarget.value);
+ }
+
+ return;
+ }
+
+ if (isEscape(event.key)) {
+ onEscape?.(event);
+
+ event.currentTarget.value = toInputValue(value);
+ event.currentTarget.blur();
+ }
+ }
+
+ /** Focuses the input on mount */
+ useEffect(() => {
+ const input = inputRef.current;
+ if (!input) return;
+
+ const newValue = toInputValue(value);
+
+ if (input.value !== newValue) input.value = newValue;
+
+ if (!autoFocus && !autoSelect) return;
+
+ setTimeout(() => {
+ input.focus();
+
+ if (autoSelect) {
+ input.select();
+ }
+ }, 1);
+ }, []);
+
+ return (
+
+ .
+ onChange?.(event, event.target.value)}
+ onChange={handleInput}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ ref={inputRef}
+ />
+
+ );
+}
diff --git a/tgui/packages/tgui/components/TextArea.jsx b/tgui/packages/tgui/components/TextArea.jsx
deleted file mode 100644
index 4a3a5145ff93..000000000000
--- a/tgui/packages/tgui/components/TextArea.jsx
+++ /dev/null
@@ -1,173 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @author Warlockd
- * @license MIT
- */
-
-import { KEY_ESCAPE } from 'common/keycodes';
-import { classes } from 'common/react';
-import { Component, createRef } from 'react';
-
-import { Box } from './Box';
-import { toInputValue } from './Input';
-
-export class TextArea extends Component {
- constructor(props) {
- super(props);
- this.textareaRef = createRef();
- this.fillerRef = createRef();
- this.state = {
- editing: false,
- };
- const { dontUseTabForIndent = false } = props;
- this.handleOnInput = (e) => {
- const { editing } = this.state;
- const { onInput } = this.props;
- if (!editing) {
- this.setEditing(true);
- }
- if (onInput) {
- onInput(e, e.target.value);
- }
- };
- this.handleOnChange = (e) => {
- const { editing } = this.state;
- const { onChange } = this.props;
- if (editing) {
- this.setEditing(false);
- }
- if (onChange) {
- onChange(e, e.target.value);
- }
- };
- this.handleKeyPress = (e) => {
- const { editing } = this.state;
- const { onKeyPress } = this.props;
- if (!editing) {
- this.setEditing(true);
- }
- if (onKeyPress) {
- onKeyPress(e, e.target.value);
- }
- };
- this.handleKeyDown = (e) => {
- const { editing } = this.state;
- const { onKeyDown } = this.props;
- if (e.keyCode === KEY_ESCAPE) {
- this.setEditing(false);
- e.target.value = toInputValue(this.props.value);
- e.target.blur();
- return;
- }
- if (!editing) {
- this.setEditing(true);
- }
- if (!dontUseTabForIndent) {
- const keyCode = e.keyCode || e.which;
- if (keyCode === 9) {
- e.preventDefault();
- const { value, selectionStart, selectionEnd } = e.target;
- e.target.value =
- value.substring(0, selectionStart) +
- '\t' +
- value.substring(selectionEnd);
- e.target.selectionEnd = selectionStart + 1;
- }
- }
- if (onKeyDown) {
- onKeyDown(e, e.target.value);
- }
- };
- this.handleFocus = (e) => {
- const { editing } = this.state;
- if (!editing) {
- this.setEditing(true);
- }
- };
- this.handleBlur = (e) => {
- const { editing } = this.state;
- const { onChange } = this.props;
- if (editing) {
- this.setEditing(false);
- if (onChange) {
- onChange(e, e.target.value);
- }
- }
- };
- }
-
- componentDidMount() {
- const nextValue = this.props.value;
- const input = this.textareaRef.current;
- if (input) {
- input.value = toInputValue(nextValue);
- }
-
- if (this.props.autoFocus || this.props.autoSelect) {
- setTimeout(() => {
- input.focus();
-
- if (this.props.autoSelect) {
- input.select();
- }
- }, 1);
- }
- }
-
- componentDidUpdate(prevProps, prevState) {
- const { editing } = this.state;
- const prevValue = prevProps.value;
- const nextValue = this.props.value;
- const input = this.textareaRef.current;
- if (input && !editing && prevValue !== nextValue) {
- input.value = toInputValue(nextValue);
- }
- }
-
- setEditing(editing) {
- this.setState({ editing });
- }
-
- getValue() {
- return this.textareaRef.current && this.textareaRef.current.value;
- }
-
- render() {
- // Input only props
- const {
- onChange,
- onKeyDown,
- onKeyPress,
- onInput,
- onFocus,
- onBlur,
- onEnter,
- value,
- maxLength,
- placeholder,
- ...boxProps
- } = this.props;
- // Box props
- const { className, fluid, ...rest } = boxProps;
- return (
-
-
-
- );
- }
-}
diff --git a/tgui/packages/tgui/components/TextArea.tsx b/tgui/packages/tgui/components/TextArea.tsx
new file mode 100644
index 000000000000..0482229b8fd4
--- /dev/null
+++ b/tgui/packages/tgui/components/TextArea.tsx
@@ -0,0 +1,198 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @author Warlockd
+ * @license MIT
+ */
+
+import { isEscape, KEY } from 'common/keys';
+import { classes } from 'common/react';
+import {
+ forwardRef,
+ RefObject,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from 'react';
+import { KeyboardEvent, SyntheticEvent } from 'react';
+
+import { Box, BoxProps } from './Box';
+import { toInputValue } from './Input';
+
+type Props = Partial<{
+ autoFocus: boolean;
+ autoSelect: boolean;
+ displayedValue: string;
+ dontUseTabForIndent: boolean;
+ fluid: boolean;
+ maxLength: number;
+ noborder: boolean;
+ /** Fires when user is 'done typing': Clicked out, blur, enter key (but not shift+enter) */
+ onChange: (event: SyntheticEvent, value: string) => void;
+ /** Fires once the enter key is pressed */
+ onEnter: (event: SyntheticEvent, value: string) => void;
+ /** Fires once the escape key is pressed */
+ onEscape: (event: SyntheticEvent) => void;
+ /** Fires on each key press / value change. Used for searching */
+ onInput: (event: SyntheticEvent, value: string) => void;
+ placeholder: string;
+ scrollbar: boolean;
+ selfClear: boolean;
+ value: string;
+}> &
+ BoxProps;
+
+export const TextArea = forwardRef(
+ (props: Props, forwardedRef: RefObject) => {
+ const {
+ autoFocus,
+ autoSelect,
+ displayedValue,
+ dontUseTabForIndent,
+ maxLength,
+ noborder,
+ onChange,
+ onEnter,
+ onEscape,
+ onInput,
+ placeholder,
+ scrollbar,
+ selfClear,
+ value,
+ ...boxProps
+ } = props;
+ const { className, fluid, nowrap, ...rest } = boxProps;
+
+ const textareaRef = useRef(null);
+ const [scrolledAmount, setScrolledAmount] = useState(0);
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === KEY.Enter) {
+ if (event.shiftKey) {
+ event.currentTarget.focus();
+ return;
+ }
+
+ onEnter?.(event, event.currentTarget.value);
+ if (selfClear) {
+ event.currentTarget.value = '';
+ }
+ event.currentTarget.blur();
+ return;
+ }
+
+ if (isEscape(event.key)) {
+ onEscape?.(event);
+ if (selfClear) {
+ event.currentTarget.value = '';
+ } else {
+ event.currentTarget.value = toInputValue(value);
+ event.currentTarget.blur();
+ }
+
+ return;
+ }
+
+ if (!dontUseTabForIndent && event.key === KEY.Tab) {
+ event.preventDefault();
+ const { value, selectionStart, selectionEnd } = event.currentTarget;
+ event.currentTarget.value =
+ value.substring(0, selectionStart) +
+ '\t' +
+ value.substring(selectionEnd);
+ event.currentTarget.selectionEnd = selectionStart + 1;
+ }
+ };
+
+ useImperativeHandle(
+ forwardedRef,
+ () => textareaRef.current as HTMLTextAreaElement,
+ );
+
+ /** Focuses the input on mount */
+ useEffect(() => {
+ if (!autoFocus && !autoSelect) return;
+
+ const input = textareaRef.current;
+ if (!input) return;
+
+ if (autoFocus || autoSelect) {
+ setTimeout(() => {
+ input.focus();
+
+ if (autoSelect) {
+ input.select();
+ }
+ }, 1);
+ }
+ }, []);
+
+ /** Updates the initial value on props change */
+ useEffect(() => {
+ const input = textareaRef.current;
+ if (!input) return;
+
+ const newValue = toInputValue(value);
+ if (input.value === newValue) return;
+
+ input.value = newValue;
+ }, [value]);
+
+ return (
+
+ {!!displayedValue && (
+
+ )}
+
+ );
+ },
+);
diff --git a/tgui/packages/tgui/interfaces/NumberInputModal.tsx b/tgui/packages/tgui/interfaces/NumberInputModal.tsx
index 2fe2165a7bce..73d83ca3a8a8 100644
--- a/tgui/packages/tgui/interfaces/NumberInputModal.tsx
+++ b/tgui/packages/tgui/interfaces/NumberInputModal.tsx
@@ -1,5 +1,7 @@
-import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes';
-import { useBackend, useLocalState } from '../backend';
+import { isEscape, KEY } from 'common/keys';
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
import { Box, Button, RestrictedInput, Section, Stack } from '../components';
import { Window } from '../layouts';
import { InputButtons } from './common/InputButtons';
@@ -11,26 +13,23 @@ type NumberInputData = {
max_value: number | null;
message: string;
min_value: number | null;
+ round_value: boolean;
timeout: number;
title: string;
};
-export const NumberInputModal = (_) => {
+export const NumberInputModal = (props) => {
const { act, data } = useBackend();
const { init_value, large_buttons, message = '', timeout, title } = data;
- const [input, setInput] = useLocalState('input', init_value);
- const onChange = (value: number) => {
- if (value === input) {
- return;
- }
- setInput(value);
- };
- const onClick = (value: number) => {
+ const [input, setInput] = useState(init_value);
+
+ const setValue = (value: number) => {
if (value === input) {
return;
}
setInput(value);
};
+
// Dynamically changes the window height based on the message.
const windowHeight =
140 +
@@ -42,11 +41,10 @@ export const NumberInputModal = (_) => {
{timeout && }
{
- const keyCode = window.event ? event.which : event.keyCode;
- if (keyCode === KEY_ENTER) {
+ if (event.key === KEY.Enter) {
act('submit', { entry: input });
}
- if (keyCode === KEY_ESCAPE) {
+ if (isEscape(event.key)) {
act('cancel');
}
}}
@@ -57,7 +55,12 @@ export const NumberInputModal = (_) => {
{message}
-
+
@@ -72,8 +75,8 @@ export const NumberInputModal = (_) => {
/** Gets the user input and invalidates if there's a constraint. */
const InputArea = (props) => {
const { act, data } = useBackend();
- const { min_value, max_value, init_value } = data;
- const { input, onClick, onChange } = props;
+ const { min_value, max_value, init_value, round_value } = data;
+ const { input, onClick, onChange, onBlur } = props;
return (
@@ -90,9 +93,11 @@ const InputArea = (props) => {
autoFocus
autoSelect
fluid
+ allowFloats={!round_value}
minValue={min_value}
maxValue={max_value}
onChange={(_, value) => onChange(value)}
+ onBlur={(_, value) => onBlur(value)}
onEnter={(_, value) => act('submit', { entry: value })}
value={input}
/>
diff --git a/tgui/packages/tgui/interfaces/TextInputModal.tsx b/tgui/packages/tgui/interfaces/TextInputModal.tsx
index c92db58957a9..980fa6db797f 100644
--- a/tgui/packages/tgui/interfaces/TextInputModal.tsx
+++ b/tgui/packages/tgui/interfaces/TextInputModal.tsx
@@ -1,5 +1,7 @@
-import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes';
-import { useBackend, useLocalState } from '../backend';
+import { isEscape, KEY } from 'common/keys';
+import { KeyboardEvent, useState } from 'react';
+
+import { useBackend } from '../backend';
import { Box, Section, Stack, TextArea } from '../components';
import { Window } from '../layouts';
import { InputButtons } from './common/InputButtons';
@@ -15,29 +17,43 @@ type TextInputData = {
title: string;
};
-export const TextInputModal = (_) => {
+export const sanitizeMultiline = (toSanitize: string) => {
+ return toSanitize.replace(/(\n|\r\n){3,}/, '\n\n');
+};
+
+export const removeAllSkiplines = (toSanitize: string) => {
+ return toSanitize.replace(/[\r\n]+/, '');
+};
+
+export const TextInputModal = (props) => {
const { act, data } = useBackend();
const {
large_buttons,
max_length,
message = '',
multiline,
- placeholder,
+ placeholder = '',
timeout,
title,
} = data;
- const [input, setInput] = useLocalState('input', placeholder || '');
+
+ const [input, setInput] = useState(placeholder || '');
const onType = (value: string) => {
if (value === input) {
return;
}
- setInput(value);
+ const sanitizedInput = multiline
+ ? sanitizeMultiline(value)
+ : removeAllSkiplines(value);
+ setInput(sanitizedInput);
};
+
+ const visualMultiline = multiline || input.length >= 30;
// Dynamically changes the window height based on the message.
const windowHeight =
135 +
(message.length > 30 ? Math.ceil(message.length / 4) : 0) +
- (multiline || input.length >= 30 ? 75 : 0) +
+ (visualMultiline ? 75 : 0) +
(message.length && large_buttons ? 5 : 0);
return (
@@ -45,11 +61,13 @@ export const TextInputModal = (_) => {
{timeout && }
{
- const keyCode = window.event ? event.which : event.keyCode;
- if (keyCode === KEY_ENTER) {
+ if (
+ event.key === KEY.Enter &&
+ (!visualMultiline || !event.shiftKey)
+ ) {
act('submit', { entry: input });
}
- if (keyCode === KEY_ESCAPE) {
+ if (isEscape(event.key)) {
act('cancel');
}
}}
@@ -60,7 +78,7 @@ export const TextInputModal = (_) => {
{message}
-
+
{
};
/** Gets the user input and invalidates if there's a constraint. */
-const InputArea = (props) => {
+const InputArea = (props: {
+ input: string;
+ onType: (value: string) => void;
+}) => {
const { act, data } = useBackend();
const { max_length, multiline } = data;
const { input, onType } = props;
+ const visualMultiline = multiline || input.length >= 30;
+
return (