Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds a Ranked list input modal #26926

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions code/__HELPERS/lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -920,3 +920,10 @@
while(islist(result))
result = pickweight(fill_with_ones(result))
return result

/**
* Checks to make sure that the lists have the exact same contents, ignores the order of the contents.
*/
/proc/lists_equal_unordered(list/list_one, list/list_two)
// This ensures that both lists contain the same elements by checking if the difference between them is empty in both directions.
return !length(list_one ^ list_two)
14 changes: 11 additions & 3 deletions code/modules/tgui/tgui_input/list_input.dm
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
var/datum/ui_state/state
/// Whether the tgui list input is invalid or not (i.e. due to all list entries being null)
var/invalid = FALSE
/// The TGUI modal to use for this popup
var/modal_type = "ListInputModal"

/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout, ui_state)
src.title = title
Expand Down Expand Up @@ -122,7 +124,7 @@
/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ListInputModal")
ui = new(user, src, modal_type)
ui.set_autoupdate(FALSE)
ui.open()

Expand Down Expand Up @@ -152,9 +154,8 @@

switch(action)
if("submit")
if(!(params["entry"] in items))
if(!handle_submit_action(params))
return
set_choice(items_map[params["entry"]])
closed = TRUE
SStgui.close_uis(src)
return TRUE
Expand All @@ -163,5 +164,12 @@
SStgui.close_uis(src)
return TRUE


/datum/tgui_list_input/proc/handle_submit_action(params)
if(!(params["entry"] in items))
return FALSE
set_choice(items_map[params["entry"]])
return TRUE

/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
56 changes: 56 additions & 0 deletions code/modules/tgui/tgui_input/ranked_list_input.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Creates a TGUI input list window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the input box to.
* * 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_ranked_list(mob/user, message, title = "Select", list/items, default, timeout = 0, ui_state = GLOB.always_state)
if(!user)
user = usr

if(!length(items))
CRASH("[user] tried to open an empty TGUI Input List. Contents are: [items]")

if(!istype(user))
if(!isclient(user))
CRASH("We passed something that wasn't a user/client in a TGUI Input List! The passed user was [user]!")
var/client/client = user
user = client.mob

if(isnull(user.client))
return

// We don't support disabled TGUI input (PREFTOGGLE_2_DISABLE_TGUI_INPUT), get with the times old man

var/datum/tgui_list_input/ranked/input = new(user, message, title, items, default, timeout, ui_state)

if(input.invalid)
qdel(input)
return

input.ui_interact(user)
input.wait()
if(input)
. = input.choice
qdel(input)

/**
* # tgui_list_input/ranked
*
* Datum used for allowing a user to sort a TGUI-controlled list input that prompts the user with
* a message and shows a list of rankable options
*/
/datum/tgui_list_input/ranked
modal_type = "RankedListInputModal"

/datum/tgui_list_input/ranked/handle_submit_action(params)
if(!lists_equal_unordered(params["entry"], items))
return FALSE
set_choice(params["entry"])
return TRUE
1 change: 1 addition & 0 deletions paradise.dme
Original file line number Diff line number Diff line change
Expand Up @@ -2944,6 +2944,7 @@
#include "code\modules\tgui\tgui_input\keycombo_input.dm"
#include "code\modules\tgui\tgui_input\list_input.dm"
#include "code\modules\tgui\tgui_input\number_input.dm"
#include "code\modules\tgui\tgui_input\ranked_list_input.dm"
#include "code\modules\tgui\tgui_input\text_input.dm"
#include "code\modules\tgui\tgui_panel\audio.dm"
#include "code\modules\tgui\tgui_panel\telemetry.dm"
Expand Down
116 changes: 116 additions & 0 deletions tgui/packages/tgui/interfaces/RankedListInputModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Loader } from './common/Loader';
import { InputButtons } from './common/InputButtons';
import { Button, Section, Stack, Table } from '../components';
import { useBackend, useLocalState } from '../backend';
import { Window } from '../layouts';
import { TableRow } from '../components/Table';

type ListInputData = {
items: string[];
message: string;
timeout: number;
title: string;
};

export const RankedListInputModal = (props, context) => {
const { act, data } = useBackend<ListInputData>(context);
const { items = [], message = '', timeout, title } = data;
const [edittedItems, setEdittedItems] = useLocalState<string[]>(context, 'edittedItems', items);

// Dynamically changes the window height based on the message.
const windowHeight = 330 + Math.ceil(message.length / 3);

return (
<Window title={title} width={325} height={windowHeight}>
{timeout && <Loader value={timeout} />}
<Window.Content>
<Section className="ListInput__Section" fill title={message}>
<Stack fill vertical>
<Stack.Item grow>
<ListDisplay filteredItems={edittedItems} setEdittedItems={setEdittedItems} />
</Stack.Item>
<Stack.Item mt={0.5}>
<InputButtons input={edittedItems} />
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};

/**
* Displays the list of selectable items.
* If a search query is provided, filters the items.
*/
const ListDisplay = (props, context) => {
const { filteredItems, setEdittedItems } = props;
const [draggedItemIndex, setDraggedItemIndex] = useLocalState<number | null>(context, 'draggedItemIndex', null);

// Handle the drag start event
const handleDragStart = (index: number) => {
setDraggedItemIndex(index);
};

// Handle the drag over event
const handleDragOver = (event: DragEvent) => {
event.preventDefault(); // Required to allow dropping
};

// Handle the drop event for items
const handleDrop = (index: number | null = null) => {
if (draggedItemIndex === null) return;

const updatedItems = [...filteredItems];
const draggedItem = updatedItems.splice(draggedItemIndex, 1)[0]; // Remove dragged item

// If no index is provided, add the item to the end of the list (used for drop on section)
if (index === null) {
updatedItems.push(draggedItem);
} else {
updatedItems.splice(index, 0, draggedItem); // Insert dragged item at new position
}

setEdittedItems(updatedItems);
setDraggedItemIndex(null); // Reset the dragged item index
};

return (
<Section
fill
scrollable
tabIndex={0}
onDrop={() => handleDrop(null)} // Handle drop on Section
onDragOver={handleDragOver} // Allow dropping on Section
>
<Table>
{filteredItems.map((item, index) => (
<TableRow
key={index}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={handleDragOver}
onDrop={() => handleDrop(index)}
style={{
padding: '8px',
}}
>
<Button
fluid
py="0.25rem"
color="transparent"
style={{
animation: 'none',
transition: 'none',
cursor: 'move',
}}
icon="grip-lines"
>
{item.replace(/^\w/, (c) => c.toUpperCase())}
</Button>
</TableRow>
))}
</Table>
</Section>
);
};
42 changes: 21 additions & 21 deletions tgui/public/tgui.bundle.js

Large diffs are not rendered by default.