diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui/tgui_input_list.dm index d62014ae091..1838692dcb8 100644 --- a/code/modules/tgui/tgui_input_list.dm +++ b/code/modules/tgui/tgui_input_list.dm @@ -7,9 +7,10 @@ * * message - The content of the input box, shown in the body of the TGUI window. * * title - The title of the input box, shown on the top of the TGUI window. * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - If an option is already preselected on the UI. Current values, etc. * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list(mob/user, message, title = "Select", list/items, timeout = 0) +/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0) if (!user) user = usr if(!length(items)) @@ -23,7 +24,7 @@ /// Client does NOT have tgui_input on: Returns regular input if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title) as null|anything in items - var/datum/tgui_list_input/input = new(user, message, title, items, timeout) + var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) input.ui_interact(user) input.wait() if (input) @@ -39,10 +40,11 @@ * * message - The content of the input box, shown in the body of the TGUI window. * * title - The title of the input box, shown on the top of the TGUI window. * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - If an option is already preselected on the UI. Current values, etc. * * callback - The callback to be invoked when a choice is made. * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list_async(mob/user, message, title, list/items, datum/callback/callback, timeout = 60 SECONDS) +/proc/tgui_input_list_async(mob/user, message, title = "Select", list/items, default, datum/callback/callback, timeout = 60 SECONDS) if (!user) user = usr if(!length(items)) @@ -53,7 +55,10 @@ user = client.mob else return - var/datum/tgui_list_input/async/input = new(user, message, title, items, callback, timeout) + /// Client does NOT have tgui_input on: Returns regular input + if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + return input(user, message, title) as null|anything in items + var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout) input.ui_interact(user) /** @@ -73,6 +78,8 @@ var/list/items_map /// The button that the user has pressed, null if no selection has been made var/choice + /// The default button to be selected + var/default /// The time at which the tgui_list_input was created, for displaying timeout progress. var/start_time /// The lifespan of the tgui_list_input, after which the window will close and delete itself. @@ -80,11 +87,12 @@ /// Boolean field describing if the tgui_list_input was closed by the user. var/closed -/datum/tgui_list_input/New(mob/user, message, title, list/items, timeout) +/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout) src.title = title src.message = message src.items = list() src.items_map = list() + src.default = default var/list/repeat_items = list() // Gets rid of illegal characters @@ -136,6 +144,7 @@ /datum/tgui_list_input/ui_static_data(mob/user) . = list( + "init_value" = default || items[1], "items" = items, "message" = message, "preferences" = list(), @@ -177,8 +186,8 @@ /// The callback to be invoked by the tgui_list_input upon having a choice made. var/datum/callback/callback -/datum/tgui_list_input/async/New(mob/user, message, title, list/items, callback, timeout) - ..(user, message, title, items, timeout) +/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout) + ..(user, message, title, items, default, timeout) src.callback = callback /datum/tgui_list_input/async/Destroy(force, ...) diff --git a/code/modules/tgui/tgui_input_number.dm b/code/modules/tgui/tgui_input_number.dm index fcd768f1e73..1d908fd54fd 100644 --- a/code/modules/tgui/tgui_input_number.dm +++ b/code/modules/tgui/tgui_input_number.dm @@ -14,7 +14,7 @@ * * min_value - Specifies a minimum value. Often 0. * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_number(mob/user, message = null, title = "Number Input", default = null, max_value = null, min_value = 0, timeout = 0) +/proc/tgui_input_number(mob/user, message, title = "Number Input", default, max_value, min_value, timeout = 0) if (!user) user = usr if (!istype(user)) @@ -23,7 +23,7 @@ user = client.mob else return - /// Client does NOT have tgui_input on: Returns regular input + // Client does NOT have tgui_input on: Returns regular input if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) return input(user, message, title, default) as null|num var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout) @@ -48,7 +48,7 @@ * * callback - The callback to be invoked when a choice is made. * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_number_async(mob/user, message = null, title = "Number Input", default = null, max_value = null, min_value = 0, datum/callback/callback, timeout = 60 SECONDS) +/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default, max_value, min_value, datum/callback/callback, timeout = 60 SECONDS) if (!user) user = usr if (!istype(user)) @@ -57,6 +57,9 @@ user = client.mob else return + // Client does NOT have tgui_input on: Returns regular input + if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + return input(user, message, title, default) as null|num var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout) number_input.ui_interact(user) @@ -126,10 +129,10 @@ /datum/tgui_input_number/ui_static_data(mob/user) . = list( + "init_value" = default || 0, // Default is a reserved keyword "max_value" = max_value, "message" = message, - "min_value" = min_value, - "placeholder" = default, /// You cannot use default as a const + "min_value" = min_value || 0, "preferences" = list(), "title" = title ) diff --git a/tgui/packages/tgui/interfaces/ListInputModal.tsx b/tgui/packages/tgui/interfaces/ListInputModal.tsx index 5a758a52d64..0f2fe0b7db7 100644 --- a/tgui/packages/tgui/interfaces/ListInputModal.tsx +++ b/tgui/packages/tgui/interfaces/ListInputModal.tsx @@ -1,13 +1,14 @@ import { Loader } from './common/Loader'; -import { InputButtons, Preferences, Validator } from './common/InputButtons'; +import { InputButtons, Preferences } from './common/InputButtons'; import { Button, Input, Section, Stack } from '../components'; -import { KEY_ENTER, KEY_DOWN, KEY_UP, KEY_ESCAPE } from '../../common/keycodes'; +import { KEY_A, KEY_DOWN, KEY_ESCAPE, KEY_ENTER, KEY_UP, KEY_Z } from '../../common/keycodes'; import { Window } from '../layouts'; import { useBackend, useLocalState } from '../backend'; type ListInputData = { items: string[]; message: string; + init_value: string; preferences: Preferences; timeout: number; title: string; @@ -15,12 +16,12 @@ type ListInputData = { export const ListInputModal = (_, context) => { const { act, data } = useBackend(context); - const { items = [], message, preferences, timeout, title } = data; + const { items = [], message, init_value, preferences, timeout, title } = data; const { large_buttons } = preferences; - const [selected, setSelected] = useLocalState( + const [selected, setSelected] = useLocalState( context, 'selected', - 0 + items.indexOf(init_value) ); const [searchBarVisible, setSearchBarVisible] = useLocalState( context, @@ -32,56 +33,60 @@ export const ListInputModal = (_, context) => { 'searchQuery', '' ); - const [inputIsValid, setInputIsValid] = useLocalState( - context, - 'inputIsValid', - { isValid: true, error: null } - ); // User presses up or down on keyboard // Simulates clicking an item const onArrowKey = (key: number) => { const len = filteredItems.length - 1; if (key === KEY_DOWN) { if (selected === null || selected === len) { - onClick(0); + setSelected(0); + document!.getElementById('0')?.scrollIntoView(); } else { - onClick(selected + 1); + setSelected(selected + 1); + document!.getElementById((selected + 1).toString())?.scrollIntoView(); } } else if (key === KEY_UP) { if (selected === null || selected === 0) { - onClick(len); + setSelected(len); + document!.getElementById(len.toString())?.scrollIntoView(); } else { - onClick(selected - 1); + setSelected(selected - 1); + document!.getElementById((selected - 1).toString())?.scrollIntoView(); } } }; // User selects an item with mouse const onClick = (index: number) => { - if (isNaN(index) || index === selected) { - setInputIsValid({ isValid: false, error: 'No selection' }); - setSelected(null); - } else { - setInputIsValid({ isValid: true, error: null }); - setSelected(index); - document!.getElementById(index.toString())?.focus(); + if (index === selected) { + return; } + setSelected(index); }; - // User doesn't have search bar visible & presses a key - const onLetterKey = (key: number) => { + // User presses a letter key and searchbar is visible + const onFocusSearch = () => { + setSearchBarVisible(false); + setSearchBarVisible(true); + }; + // User presses a letter key with no searchbar visible + const onLetterSearch = (key: number) => { const keyChar = String.fromCharCode(key); const foundItem = items.find((item) => { return item?.toLowerCase().startsWith(keyChar?.toLowerCase()); }); if (foundItem) { - setSelected(filteredItems.indexOf(foundItem)); - document!.getElementById(filteredItems - .indexOf(foundItem) - .toString())?.focus(); + const foundIndex = items.indexOf(foundItem); + setSelected(foundIndex); + document!.getElementById(foundIndex.toString())?.scrollIntoView(); } }; // User types into search bar const onSearch = (query: string) => { + if (query === searchQuery) { + return; + } setSearchQuery(query); + setSelected(0); + document!.getElementById('0')?.scrollIntoView(); }; // User presses the search button const onSearchBarToggle = () => { @@ -94,6 +99,10 @@ export const ListInputModal = (_, context) => { // Dynamically changes the window height based on the message. const windowHeight = 325 + Math.ceil(message?.length / 3) + (large_buttons ? 5 : 0); + // Grabs the cursor when no search bar is visible. + if (!searchBarVisible) { + setTimeout(() => document!.getElementById(selected.toString())?.focus(), 1); + } return ( @@ -105,9 +114,13 @@ export const ListInputModal = (_, context) => { event.preventDefault(); onArrowKey(keyCode); } - if (!searchBarVisible && keyCode >= 65 && keyCode <= 90) { + if (keyCode === KEY_ENTER) { event.preventDefault(); - onLetterKey(keyCode); + act('submit', { entry: filteredItems[selected] }); + } + if (!searchBarVisible && keyCode >= KEY_A && keyCode <= KEY_Z) { + event.preventDefault(); + onLetterSearch(keyCode); } if (keyCode === KEY_ESCAPE) { event.preventDefault(); @@ -118,13 +131,15 @@ export const ListInputModal = (_, context) => { buttons={ @@ -63,9 +63,9 @@ export const InputButtons = (props: InputButtonsProps, context) => { )} {!large_buttons && ( - {!isValid && ( + {inputIsValid && !inputIsValid.isValid && inputIsValid.error && ( - {error} + {inputIsValid.error} )}