TextBox Shift-Enter (Multilines)

This commit is contained in:
ItsSelis
2023-05-19 22:49:46 +02:00
parent f7eb1e0ec8
commit 7ddff379c7
5 changed files with 99 additions and 27 deletions

View File

@@ -122,8 +122,8 @@
data["multiline"] = multiline data["multiline"] = multiline
data["placeholder"] = default // Default is a reserved keyword data["placeholder"] = default // Default is a reserved keyword
data["swapped_buttons"] = !user.client.prefs.tgui_swapped_buttons data["swapped_buttons"] = !user.client.prefs.tgui_swapped_buttons
// CHOMPedit - prevent_enter should be completely removed in the future
data["title"] = title data["title"] = title
data["prevent_enter"] = prevent_enter
return data return data
/datum/tgui_input_text/tgui_data(mob/user) /datum/tgui_input_text/tgui_data(mob/user)

View File

@@ -15,9 +15,10 @@ export class TextArea extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.textareaRef = props.innerRef || createRef(); this.textareaRef = props.innerRef || createRef();
this.fillerRef = createRef(); // CHOMPedit
this.state = { this.state = {
editing: false, editing: false,
scrolledAmount: 0,
}; };
const { dontUseTabForIndent = false } = props; const { dontUseTabForIndent = false } = props;
this.handleOnInput = (e) => { this.handleOnInput = (e) => {
@@ -52,7 +53,8 @@ export class TextArea extends Component {
}; };
this.handleKeyDown = (e) => { this.handleKeyDown = (e) => {
const { editing } = this.state; const { editing } = this.state;
const { onChange, onInput, onEnter, onKeyDown } = this.props; // CHOMPedit
const { onChange, onInput, onEnter, onKey } = this.props;
if (e.keyCode === KEY_ENTER) { if (e.keyCode === KEY_ENTER) {
this.setEditing(false); this.setEditing(false);
if (onChange) { if (onChange) {
@@ -86,8 +88,10 @@ export class TextArea extends Component {
if (!editing) { if (!editing) {
this.setEditing(true); this.setEditing(true);
} }
if (onKeyDown) { // CHOMPedit
onKeyDown(e, e.target.value); // Custom key handler
if (onKey) {
onKey(e, e.target.value);
} }
if (!dontUseTabForIndent) { if (!dontUseTabForIndent) {
const keyCode = e.keyCode || e.which; const keyCode = e.keyCode || e.which;
@@ -96,6 +100,10 @@ export class TextArea extends Component {
const { value, selectionStart, selectionEnd } = e.target; const { value, selectionStart, selectionEnd } = e.target;
e.target.value = value.substring(0, selectionStart) + '\t' + value.substring(selectionEnd); e.target.value = value.substring(0, selectionStart) + '\t' + value.substring(selectionEnd);
e.target.selectionEnd = selectionStart + 1; e.target.selectionEnd = selectionStart + 1;
// CHOMPedit
if (onInput) {
onInput(e, e.target.value);
}
} }
} }
}; };
@@ -115,6 +123,16 @@ export class TextArea extends Component {
} }
} }
}; };
// CHOMPedit Start
this.handleScroll = (e) => {
const { displayedValue } = this.props;
const input = this.textareaRef.current;
if (displayedValue && input) {
this.setState({
scrolledAmount: input.scrollTop,
});
}
};
} }
componentDidMount() { componentDidMount() {
@@ -123,8 +141,15 @@ export class TextArea extends Component {
if (input) { if (input) {
input.value = toInputValue(nextValue); input.value = toInputValue(nextValue);
} }
if (this.props.autoFocus) { if (this.props.autoFocus || this.props.autoSelect) {
setTimeout(() => input.focus(), 1); setTimeout(() => {
input.focus();
if (this.props.autoSelect) {
input.select();
}
}, 1);
// CHOMPedit End
} }
} }
@@ -158,15 +183,37 @@ export class TextArea extends Component {
value, value,
maxLength, maxLength,
placeholder, placeholder,
// CHOMPedit Start
scrollbar,
noborder,
displayedValue,
...boxProps ...boxProps
} = this.props; } = this.props;
// Box props // Box props
const { className, fluid, ...rest } = boxProps; const { className, fluid, nowrap, ...rest } = boxProps;
const { scrolledAmount } = this.state;
return ( return (
<Box className={classes(['TextArea', fluid && 'TextArea--fluid', className])} {...rest}> <Box
className={classes(['TextArea', fluid && 'TextArea--fluid', noborder && 'TextArea--noborder', className])}
{...rest}>
{!!displayedValue && (
<Box position="absolute" width="100%" height="100%" overflow="hidden">
<div
className={classes(['TextArea__textarea', 'TextArea__textarea_custom'])}
style={{
'transform': `translateY(-${scrolledAmount}px)`,
}}>
{displayedValue}
</div>
</Box>
)}
<textarea <textarea
ref={this.textareaRef} ref={this.textareaRef}
className="TextArea__textarea" className={classes([
'TextArea__textarea',
scrollbar && 'TextArea__textarea--scrollable',
nowrap && 'TextArea__nowrap',
])}
placeholder={placeholder} placeholder={placeholder}
onChange={this.handleOnChange} onChange={this.handleOnChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
@@ -174,8 +221,13 @@ export class TextArea extends Component {
onInput={this.handleOnInput} onInput={this.handleOnInput}
onFocus={this.handleFocus} onFocus={this.handleFocus}
onBlur={this.handleBlur} onBlur={this.handleBlur}
onScroll={this.handleScroll}
maxLength={maxLength} maxLength={maxLength}
style={{
'color': displayedValue ? 'rgba(0, 0, 0, 0)' : 'inherit',
}}
/> />
{/* CHOMPedit End */}
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,7 @@
import { Loader } from './common/Loader'; import { Loader } from './common/Loader';
import { InputButtons } from './common/InputButtons'; import { InputButtons } from './common/InputButtons';
import { useBackend, useLocalState } from '../backend'; import { useBackend, useLocalState } from '../backend';
import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes'; // CHOMPedit
import { Box, Section, Stack, TextArea } from '../components'; import { Box, Section, Stack, TextArea } from '../components';
import { Window } from '../layouts'; import { Window } from '../layouts';
@@ -12,37 +13,51 @@ type TextInputData = {
placeholder: string; placeholder: string;
timeout: number; timeout: number;
title: string; title: string;
prevent_enter: boolean; // CHOMPedit Start
};
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, context) => { export const TextInputModal = (props, context) => {
const { act, data } = useBackend<TextInputData>(context); const { act, data } = useBackend<TextInputData>(context);
const { large_buttons, max_length, message = '', multiline, placeholder, timeout, title, prevent_enter } = data; const { large_buttons, max_length, message = '', multiline, placeholder, timeout, title } = data;
const [input, setInput] = useLocalState<string>(context, 'input', placeholder || ''); const [input, setInput] = useLocalState<string>(context, 'input', placeholder || '');
const onType = (value: string) => { const onType = (value: string) => {
if (value === input) { if (value === input) {
return; 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. // Dynamically changes the window height based on the message.
const windowHeight = const windowHeight =
135 + 135 +
(message.length > 30 ? Math.ceil(message.length / 4) : 0) + (message.length > 30 ? Math.ceil(message.length / 4) : 0) +
(multiline || input.length >= 30 ? 75 : 0) + (visualMultiline ? 75 : 0) +
(message.length && large_buttons ? 5 : 0); (message.length && large_buttons ? 5 : 0);
return ( return (
<Window title={title} width={325} height={windowHeight}> <Window title={title} width={325} height={windowHeight}>
{timeout && <Loader value={timeout} />} {timeout && <Loader value={timeout} />}
<Window.Content <Window.Content
onEscape={() => act('cancel')} onKeyDown={(event) => {
onEnter={(event) => { const keyCode = window.event ? event.which : event.keyCode;
if (!prevent_enter) { if (keyCode === KEY_ENTER && (!visualMultiline || !event.shiftKey)) {
act('submit', { entry: input }); act('submit', { entry: input });
event.preventDefault(); }
if (keyCode === KEY_ESCAPE) {
act('cancel');
} }
}}> }}>
{/* CHOMPedit End */}
<Section fill> <Section fill>
<Stack fill vertical> <Stack fill vertical>
<Stack.Item> <Stack.Item>
@@ -64,9 +79,11 @@ export const TextInputModal = (props, context) => {
/** Gets the user input and invalidates if there's a constraint. */ /** Gets the user input and invalidates if there's a constraint. */
const InputArea = (props, context) => { const InputArea = (props, context) => {
const { act, data } = useBackend<TextInputData>(context); const { act, data } = useBackend<TextInputData>(context);
const { max_length, multiline, prevent_enter } = data; const { max_length, multiline } = data; // CHOMPedit
const { input, onType } = props; const { input, onType } = props;
const visualMultiline = multiline || input.length >= 30; // CHOMPedit
return ( return (
<TextArea <TextArea
autoFocus autoFocus
@@ -75,10 +92,13 @@ const InputArea = (props, context) => {
maxLength={max_length} maxLength={max_length}
onEscape={() => act('cancel')} onEscape={() => act('cancel')}
onEnter={(event) => { onEnter={(event) => {
if (!prevent_enter) { // CHOMPedit Start
act('submit', { entry: input }); if (visualMultiline && event.shiftKey) {
event.preventDefault(); return;
} }
event.preventDefault();
act('submit', { entry: input });
// CHOMPedit End
}} }}
onInput={(_, value) => onType(value)} onInput={(_, value) => onType(value)}
placeholder="Type something..." placeholder="Type something..."

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long