mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-17 15:06:02 +01:00
@@ -1,15 +1,16 @@
|
||||
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<string | { label: string; description: string }>;
|
||||
description?: string;
|
||||
defaultIndex?: number;
|
||||
args?: any;
|
||||
args?: Record<any, any>;
|
||||
close?: boolean;
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ interface MenuProps {
|
||||
onClose?: (keyPressed?: 'Escape' | 'Backspace') => void;
|
||||
onSelected?: ChangeFunction;
|
||||
onSideScroll?: ChangeFunction;
|
||||
onChecked?: ChangeFunction;
|
||||
cb?: ChangeFunction;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@ 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<string | { label: string, description: string }>
|
||||
---@field description? string
|
||||
---@field defaultIndex? number
|
||||
---@field args? any
|
||||
---@field args? {[any]: any}
|
||||
---@field close? boolean
|
||||
|
||||
---@class MenuProps
|
||||
@@ -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
|
||||
|
||||
@@ -151,15 +153,36 @@ 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
|
||||
|
||||
if data[2] then
|
||||
|
||||
local args = openMenu.options[data[1]].args
|
||||
|
||||
if args and type(args) ~= 'table' then
|
||||
return error("Menu args must be passed as a table")
|
||||
end
|
||||
|
||||
if not args then args = {} end
|
||||
print(data[2], data[3])
|
||||
if data[2] then args[data[3]] = true end
|
||||
print(args.isScroll, args.isCheck)
|
||||
|
||||
if data[2] and not args.isCheck then
|
||||
data[2] += 1 -- scrollIndex
|
||||
end
|
||||
|
||||
openMenu.onSelected(data[1], data[2], openMenu.options[data[1]].args)
|
||||
openMenu.onSelected(data[1], data[2], args)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('changeChecked', function(data, cb)
|
||||
cb(1)
|
||||
if not openMenu?.onCheck then return end
|
||||
|
||||
data[1] += 1 -- selected
|
||||
|
||||
openMenu.onCheck(data[1], data[2], openMenu.options[data[1]].args)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('closeMenu', function(data, cb)
|
||||
|
||||
@@ -14,6 +14,7 @@ export const debugMenu = () => {
|
||||
label: 'Option 2',
|
||||
icon: 'basket-shopping',
|
||||
description: 'Tooltip description 1',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
label: 'Vehicle class',
|
||||
|
||||
36
web/src/features/menu/list/CustomCheckbox.tsx
Normal file
36
web/src/features/menu/list/CustomCheckbox.tsx
Normal file
@@ -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 (
|
||||
<chakra.label
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
gridColumnGap={2}
|
||||
pr={3}
|
||||
cursor="pointer"
|
||||
{...htmlProps}
|
||||
>
|
||||
<input {...getInputProps()} hidden />
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
border="2px solid"
|
||||
borderColor="#909296"
|
||||
rounded={'sm'}
|
||||
w={5}
|
||||
h={5}
|
||||
{...getCheckboxProps()}
|
||||
>
|
||||
{checked && (
|
||||
<Box w={4} h={4} bg="#909296">
|
||||
<CheckboxIcon isChecked color="#25262B" />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</chakra.label>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomCheckbox;
|
||||
@@ -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<Array<HTMLDivElement | null>, Props>(({ item, index, scrollIndex }, ref) => {
|
||||
const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index, scrollIndex, checked }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
bg="#25262B"
|
||||
@@ -53,6 +55,11 @@ const ListItem = forwardRef<Array<HTMLDivElement | null>, Props>(({ item, index,
|
||||
<FontAwesomeIcon icon="chevron-right" fontSize={16} color="#909296" />
|
||||
</Stack>
|
||||
</Flex>
|
||||
) : item.checked !== undefined ? (
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
||||
<Text>{item.label}</Text>
|
||||
<CustomCheckbox checked={checked}></CustomCheckbox>
|
||||
</Flex>
|
||||
) : (
|
||||
<Text>{item.label}</Text>
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import React from 'react';
|
||||
|
||||
export interface MenuItem {
|
||||
label: string;
|
||||
checked?: boolean;
|
||||
values?: Array<string | { label: string; description: string }>;
|
||||
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<Record<number, number>>({});
|
||||
const [checkedStates, setCheckedStates] = useState<Record<number, boolean>>({});
|
||||
const listRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const firstRenderRef = useRef(false);
|
||||
|
||||
@@ -59,33 +61,48 @@ 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,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
break;
|
||||
case 'Enter':
|
||||
if (!menu.items[selected]) return;
|
||||
if (menu.items[selected].checked !== undefined && !menu.items[selected].values) {
|
||||
return 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, checkedStates[selected]]).catch();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [checkedStates]);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstRenderRef.current) {
|
||||
firstRenderRef.current = false;
|
||||
@@ -107,7 +124,15 @@ 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,
|
||||
menu.items[selected].values
|
||||
? indexStates[selected]
|
||||
: menu.items[selected].checked
|
||||
? checkedStates[selected]
|
||||
: null,
|
||||
menu.items[selected].values ? 'isScroll' : menu.items[selected].checked ? 'isCheck' : null,
|
||||
]).catch();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [selected, menu]);
|
||||
@@ -135,11 +160,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 +217,13 @@ const ListMenu: React.FC = () => {
|
||||
{menu.items.map((item, index) => (
|
||||
<React.Fragment key={`menu-item-${index}`}>
|
||||
{item.label && (
|
||||
<ListItem index={index} item={item} scrollIndex={indexStates[index]} ref={listRefs} />
|
||||
<ListItem
|
||||
index={index}
|
||||
item={item}
|
||||
scrollIndex={indexStates[index]}
|
||||
checked={checkedStates[index]}
|
||||
ref={listRefs}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user