diff --git a/package/client/resource/interface/menu.ts b/package/client/resource/interface/menu.ts index 258baa5..383598a 100644 --- a/package/client/resource/interface/menu.ts +++ b/package/client/resource/interface/menu.ts @@ -1,11 +1,12 @@ import { IconName, IconPrefix } from '@fortawesome/fontawesome-common-types'; type MenuPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; -type ChangeFunction = (selected: number, scrollIndex: number | null, args: any | null) => void; +type ChangeFunction = (selected: number, scrollIndex?: number, args?: any, checked?: boolean) => void; interface MenuOptions { label: string; icon?: IconName | [IconPrefix, IconName]; + checked?: boolean; values?: Array; description?: string; defaultIndex?: number; @@ -23,6 +24,7 @@ interface MenuProps { onClose?: (keyPressed?: 'Escape' | 'Backspace') => void; onSelected?: ChangeFunction; onSideScroll?: ChangeFunction; + onChecked?: ChangeFunction; cb?: ChangeFunction; } diff --git a/resource/interface/client/menu.lua b/resource/interface/client/menu.lua index 7948cb9..70cbd09 100644 --- a/resource/interface/client/menu.lua +++ b/resource/interface/client/menu.lua @@ -4,12 +4,13 @@ local registeredMenus = {} local openMenu = nil local keepInput = IsNuiFocusKeepingInput() ----@alias MenuPosition 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; ----@alias MenuChangeFunction fun(selected: number, scrollIndex?: number, args?: any) +---@alias MenuPosition 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' +---@alias MenuChangeFunction fun(selected: number, scrollIndex?: number, args?: any, checked?: boolean) ---@class MenuOptions ---@field label string ---@field icon? string +---@field checked? boolean ---@field values? Array ---@field description? string ---@field defaultIndex? number @@ -26,6 +27,7 @@ local keepInput = IsNuiFocusKeepingInput() ---@field onClose? fun(keyPressed?: 'Escape' | 'Backspace') ---@field onSelected? MenuChangeFunction ---@field onSideScroll? MenuChangeFunction +---@field onCheck? MenuChangeFunction ---@field cb? MenuChangeFunction ---@param data MenuProps @@ -132,13 +134,13 @@ RegisterNUICallback('confirmSelected', function(data, cb) end if menu.cb then - menu.cb(data[1], data[2], menu.options[data[1]].args) + menu.cb(data[1], data[2], menu.options[data[1]].args, data[3]) end end) RegisterNUICallback('changeIndex', function(data, cb) cb(1) - if not openMenu.onSideScroll then return end + if not openMenu?.onSideScroll then return end data[1] += 1 -- selected @@ -146,12 +148,12 @@ RegisterNUICallback('changeIndex', function(data, cb) data[2] += 1 -- scrollIndex end - openMenu.onSideScroll(data[1], data[2], openMenu.options[data[1]].args) + openMenu.onSideScroll(data[1], data[2], openMenu.options[data[1]].args, data[3]) end) RegisterNUICallback('changeSelected', function(data, cb) cb(1) - if not openMenu.onSelected then return end + if not openMenu?.onSelected then return end data[1] += 1 -- selected @@ -159,7 +161,20 @@ RegisterNUICallback('changeSelected', function(data, cb) data[2] += 1 -- scrollIndex end - openMenu.onSelected(data[1], data[2], openMenu.options[data[1]].args) + openMenu.onSelected(data[1], data[2], openMenu.options[data[1]].args, data[3]) +end) + +RegisterNUICallback('changeChecked', function(data, cb) + cb(1) + if not openMenu?.onCheck then return end + + data[1] += 1 -- selected + + if data[2] then + data[2] += 1 -- scrollIndex + end + + openMenu.onCheck(data[1], data[2], openMenu.options[data[1]].args, data[3]) end) RegisterNUICallback('closeMenu', function(data, cb) diff --git a/web/src/features/menu/list/CustomCheckbox.tsx b/web/src/features/menu/list/CustomCheckbox.tsx new file mode 100644 index 0000000..a2af255 --- /dev/null +++ b/web/src/features/menu/list/CustomCheckbox.tsx @@ -0,0 +1,36 @@ +import { Box, Flex, useCheckbox, chakra, CheckboxIcon } from '@chakra-ui/react'; + +const CustomCheckbox: React.FC<{ checked: boolean }> = ({ checked }) => { + const { getCheckboxProps, getInputProps, htmlProps } = useCheckbox(); + return ( + + + + {checked && ( + + + + )} + + + ) +}; + +export default CustomCheckbox; diff --git a/web/src/features/menu/list/ListItem.tsx b/web/src/features/menu/list/ListItem.tsx index 4f57bfd..8c74345 100644 --- a/web/src/features/menu/list/ListItem.tsx +++ b/web/src/features/menu/list/ListItem.tsx @@ -1,15 +1,17 @@ -import { Box, Flex, Stack, Spacer, Text, IconProps } from '@chakra-ui/react'; +import { Box, Flex, Stack, Text } from '@chakra-ui/react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { forwardRef } from 'react'; +import CustomCheckbox from './CustomCheckbox'; import type { MenuItem } from './index'; interface Props { item: MenuItem; index: number; scrollIndex: number; + checked: boolean; } -const ListItem = forwardRef, Props>(({ item, index, scrollIndex }, ref) => { +const ListItem = forwardRef, Props>(({ item, index, scrollIndex, checked }, ref) => { return ( , Props>(({ item, index, + ) : item.checked !== undefined ? ( + + {item.label} + + ) : ( {item.label} )} diff --git a/web/src/features/menu/list/index.tsx b/web/src/features/menu/list/index.tsx index fd5d1f1..c46b7d7 100644 --- a/web/src/features/menu/list/index.tsx +++ b/web/src/features/menu/list/index.tsx @@ -11,6 +11,7 @@ import React from 'react'; export interface MenuItem { label: string; + checked?: boolean; values?: Array; description?: string; icon?: IconProp; @@ -35,6 +36,7 @@ const ListMenu: React.FC = () => { const [selected, setSelected] = useState(0); const [visible, setVisible] = useState(false); const [indexStates, setIndexStates] = useState>({}); + const [checkedStates, setCheckedStates] = useState>({}); const listRefs = useRef>([]); const firstRenderRef = useRef(false); @@ -59,33 +61,63 @@ const ListMenu: React.FC = () => { }); break; case 'ArrowRight': - if (!Array.isArray(menu.items[selected].values)) return; - setIndexStates({ - ...indexStates, - [selected]: - indexStates[selected] + 1 <= menu.items[selected].values?.length! - 1 - ? indexStates[selected] + 1 - : (indexStates[selected] = 0), - }); + if (Array.isArray(menu.items[selected].values)) { + setIndexStates({ + ...indexStates, + [selected]: + indexStates[selected] + 1 <= menu.items[selected].values?.length! - 1 + ? indexStates[selected] + 1 + : 0 + }); + } else if (menu.items[selected].checked !== undefined) { + setCheckedStates({ + ...checkedStates, + [selected]: !checkedStates[selected] + }); + } break; case 'ArrowLeft': - if (!Array.isArray(menu.items[selected].values)) return; - setIndexStates({ - ...indexStates, - [selected]: - indexStates[selected] - 1 >= 0 - ? indexStates[selected] - 1 - : (indexStates[selected] = menu.items[selected].values?.length! - 1), - }); + if (Array.isArray(menu.items[selected].values)) { + setIndexStates({ + ...indexStates, + [selected]: + indexStates[selected] - 1 >= 0 + ? indexStates[selected] - 1 + : menu.items[selected].values?.length! - 1 + }); + } else if (menu.items[selected].checked !== undefined) { + setCheckedStates({ + ...checkedStates, + [selected]: !checkedStates[selected] + }); + } break; case 'Enter': if (!menu.items[selected]) return; + if (menu.items[selected].checked !== undefined) { + setCheckedStates({ + ...checkedStates, + [selected]: !checkedStates[selected] + }); + } fetchNui('confirmSelected', [selected, indexStates[selected]]).catch(); if (menu.items[selected].close === undefined || menu.items[selected].close) setVisible(false); break; } }; + useEffect(() => { + if (firstRenderRef.current) { + firstRenderRef.current = false; + return; + } + if (menu.items[selected]?.checked === undefined) return; + const timer = setTimeout(() => { + fetchNui('changeChecked', [selected, indexStates[selected], checkedStates[selected]]).catch(); + }, 100); + return () => clearTimeout(timer); + }, [checkedStates]) + useEffect(() => { if (firstRenderRef.current) { firstRenderRef.current = false; @@ -93,7 +125,7 @@ const ListMenu: React.FC = () => { } if (!menu.items[selected]?.values) return; const timer = setTimeout(() => { - fetchNui('changeIndex', [selected, indexStates[selected]]).catch(); + fetchNui('changeIndex', [selected, indexStates[selected], checkedStates[selected]]).catch(); }, 100); return () => clearTimeout(timer); }, [indexStates]); @@ -107,7 +139,7 @@ const ListMenu: React.FC = () => { listRefs.current[selected]?.focus({ preventScroll: true }); // debounces the callback to avoid spam const timer = setTimeout(() => { - fetchNui('changeSelected', [selected, indexStates[selected]]).catch(); + fetchNui('changeSelected', [selected, indexStates[selected], checkedStates[selected]]).catch(); }, 100); return () => clearTimeout(timer); }, [selected, menu]); @@ -135,11 +167,14 @@ const ListMenu: React.FC = () => { listRefs.current = []; setMenu(data); setVisible(true); - let arrayIndexes: { [key: number]: number } = {}; + const arrayIndexes: { [key: number]: number } = {}; + const checkedIndexes: { [key: number]: boolean } = {}; for (let i = 0; i < data.items.length; i++) { if (Array.isArray(data.items[i].values)) arrayIndexes[i] = (data.items[i].defaultIndex || 1) - 1; + else if (data.items[i].checked !== undefined) checkedIndexes[i] = data.items[i].checked || false; } setIndexStates(arrayIndexes); + setCheckedStates(checkedIndexes); listRefs.current[data.startItemIndex]?.focus(); }); @@ -189,7 +224,7 @@ const ListMenu: React.FC = () => { {menu.items.map((item, index) => ( {item.label && ( - + )} ))}