Skip to content

Commit

Permalink
Merge pull request #146 from BerkieBb/master
Browse files Browse the repository at this point in the history
feat(menu): checkbox
  • Loading branch information
LukeWasTakenn authored Oct 29, 2022
2 parents de23227 + 9cc7b92 commit 5b80631
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 31 deletions.
6 changes: 4 additions & 2 deletions package/client/resource/interface/menu.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -23,6 +24,7 @@ interface MenuProps {
onClose?: (keyPressed?: 'Escape' | 'Backspace') => void;
onSelected?: ChangeFunction;
onSideScroll?: ChangeFunction;
onChecked?: ChangeFunction;
cb?: ChangeFunction;
}

Expand Down
39 changes: 31 additions & 8 deletions resource/interface/client/menu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions web/src/features/dev/debug/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const debugMenu = () => {
label: 'Option 2',
icon: 'basket-shopping',
description: 'Tooltip description 1',
checked: true,
},
{
label: 'Vehicle class',
Expand Down
36 changes: 36 additions & 0 deletions web/src/features/menu/list/CustomCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 9 additions & 2 deletions web/src/features/menu/list/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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>
)}
Expand Down
72 changes: 53 additions & 19 deletions web/src/features/menu/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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;
Expand All @@ -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]);
Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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>
))}
Expand Down

0 comments on commit 5b80631

Please sign in to comment.