diff --git a/README.md b/README.md index ff262f2..de53208 100644 --- a/README.md +++ b/README.md @@ -34,26 +34,53 @@ FiveM native Rage Menu, built with React. [Documentation](https://docs.ardelanya ```lua --- you can create a menu once and reopen it at any time --- state will be cached and reused -local menu = Menu:Create('Example', 'Example Subtitle') +local menu = Menu:Create('Example', 'Example Subtitle', nil, nil, '') +-- 520 is a custom width, the default is +local submenu = Menu:Create('Submenu', 'Submenu Subtitle', 520) +submenu:AddButton('Submenu Button'):OnClick(function() + print('Submenu Button Clicked') +end) + +menu:AddButton('Button', 'Button Right Label', 'Button Description'):OnClick(function() + print('Button Clicked') +end) + +menu:AddSubmenu(submenu, 'Submenu Label', 'Right Label', 'Submenu Description') -local exampleButton = menu:AddButton('Example Button', 'Example Description', { - left = 'shop_ammo_icon' -}) +menu:AddSeparator('Separator') -exampleButton:OnClick(function(component) - print(component.label .. ' Clicked!') +local checkbox = menu:AddCheckbox('Checkbox', 'Checkbox Description', { + right = 'card_suit_hearts' +}, true) + +checkbox:OnCheck(function(checked) + print('Checkbox Checked', checked) end) -local exampleSlider = menu:AddSlider('Example Slider', 'Example Description', nil, 100, 0, 10, 50) +menu:AddButton('Disable Checkbox'):OnClick(function() + checkbox:Disable(not checkbox.disabled) + print('Checkbox Disabled', checkbox.disabled) +end) -local removeSliderHandler = exampleSlider:OnChange(function(current) - print('current slider progress', current) +menu:AddButton('Toggle Checkbox Visibility'):OnClick(function() + checkbox:ToggleVisiblity(not checkbox.visible) + print('Checkbox Visibility', checkbox.visible) end) -exampleSlider:OnClick(function() - removeSliderHandler() -- this function removes the OnChange handler, that's above +menu:AddList('List', 'List Description', { + right = 'card_suit_hearts' +}, { + 'List Item 1', + 'List Item 2', + 'List Item 3' +}, 1):OnChange(function(current, currentValue) + print('List Changed', current, currentValue) +end) - print('Removed slider `OnChange` handler') +menu:AddSlider('Slider', 'Slider Description', { + right = 'card_suit_hearts' +}, 100, 0, 10, 50):OnChange(function(current) + print('Slider Changed', current) end) RegisterCommand('example', function() diff --git a/client/main.lua b/client/main.lua index be5b1e1..a4f2215 100644 --- a/client/main.lua +++ b/client/main.lua @@ -24,7 +24,6 @@ end); exports('SetNuiFocus', SetNuiFocus); exports('SetNuiFocusKeepInput', SetNuiFocusKeepInput); - for _, callback in next, { 'OnSelect', 'OnChange', @@ -57,6 +56,7 @@ end AddEventHandler('onResourceStop', function(resource) if LAST_RESOURCE == resource then + SendNUIMessage({ action = 'SetMenu' }); SendNUIMessage({ action = 'SetItems' }); end end); diff --git a/import.lua b/import.lua index da6f079..58d65d0 100644 --- a/import.lua +++ b/import.lua @@ -62,10 +62,7 @@ local function resetNUI() SendNUIMessage({ action = 'SetMenu' }); - SendNUIMessage({ - action = 'SetItems', - data = {} - }); + SendNUIMessage({ action = 'SetItems' }); end ---@param menu Menu @@ -74,16 +71,13 @@ function Menu:Open(menu) return; end - if self.current ~= nil then - local oldMenu = self:GetById(self.current); - - if oldMenu then - oldMenu:Close(); - - self.opened[#self.opened + 1] = self.current; + for index, menuId in next, self.opened do + if menuId == menu.id then + table.remove(self.opened, index); end end + self.opened[#self.opened + 1] = menu.id; self.current = menu.id; SetNuiFocus(true, false); @@ -101,18 +95,22 @@ function Menu:Open(menu) end function Menu:Close() - if not self.current then - return; - end - if #self.opened == 0 then - self.current = nil; + return self:CloseAll(); + end - return resetNUI(); + for index, menuId in next, self.opened do + if menuId == self.current then + table.remove(self.opened, index); + end end local lastOpened = self:GetById(self.opened[#self.opened]); + if not lastOpened then + return self:CloseAll(); + end + if lastOpened then self:Open(lastOpened); end @@ -337,9 +335,11 @@ function Menu:Create(menuTitle, menuSubtitle, menuWidth, maxVisibleItems, banner id = self.id, type = self.type, label = self.label, + rightLabel = self.rightLabel, description = self.description, badges = self.badges, disabled = self.disabled, + visible = self.visible, values = self.values, checked = self.checked, current = self.current, @@ -367,9 +367,7 @@ function Menu:Create(menuTitle, menuSubtitle, menuWidth, maxVisibleItems, banner end function menu:AddSubmenu(submenu, label, rightLabel, description, badges, disabled) - local button = self:AddButton(label, rightLabel, description, badges or { - right = 'arrow_right' - }, disabled); + local button = self:AddButton(label, rightLabel, description, badges, disabled); button:OnClick(function() local subMenu = Menu:GetById(type(submenu) == 'string' and submenu or submenu.id); diff --git a/meta.lua b/meta.lua index a95a390..8eca3c6 100644 --- a/meta.lua +++ b/meta.lua @@ -53,9 +53,11 @@ ---@field id string ---@field type MenuType ---@field label string +---@field rightLabel? string ---@field description? string ---@field badges? { left?: BadgeName; right?: BadgeName } ---@field disabled? boolean +---@field visible boolean ---@field values? string[] ---@field checked? boolean ---@field current? number diff --git a/web/src/components/menu.tsx b/web/src/components/menu.tsx index 8a48b0e..45cd79e 100644 --- a/web/src/components/menu.tsx +++ b/web/src/components/menu.tsx @@ -2,10 +2,10 @@ import Item, { type ItemProps } from '@/components/item'; import SubTitle from '@/components/sub-title'; import Description from '@/components/description'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useKeyDown } from '@/lib/keys'; import { useNuiEvent } from '@/lib/hooks'; -import { cn, debugData, fetchNui, findLastIndex } from '@/lib'; +import { cn, debugData, fetchNui } from '@/lib'; interface MenuProps { id: string; @@ -58,39 +58,39 @@ export default function Menu() { setItems([...items]); }); - function ArrowUp() { - let index = Math.max(-1, selected - 1); - - if (items[index]?.type === 'separator' || items[index]?.disabled) index--; + const findNextValidIndex = useCallback( + (direction: 'up' | 'down') => { + const step = direction === 'up' ? -1 : 1; + let index = selected; + + do { + index += step; + if (index < 0) index = items.length - 1; + if (index >= items.length) index = 0; + + if ( + items[index].type !== 'separator' && + !items[index].disabled && + items[index].visible !== false + ) { + return index; + } + } while (index !== selected); - if (index < 0) - index = findLastIndex( - items, - (c) => !c.disabled && c.visible !== false && c.type !== 'separator' - ); + return -1; + }, + [items, selected] + ); - setSelected(index); + function ArrowUp() { + const newIndex = findNextValidIndex('up'); + setSelected(newIndex); } useKeyDown('ArrowUp', ArrowUp); function ArrowDown() { - let index = Math.min(items.length, selected + 1); - - if ( - items[index]?.type === 'separator' || - items[index]?.disabled || - items[index]?.visible === false - ) - index += 1; - - if (index === items.length) - index = items.findIndex( - (c) => !c.disabled && c.visible !== false && c.type !== 'separator' - ); - - if (index < 0) return; - - setSelected(index); + const newIndex = findNextValidIndex('down'); + setSelected(newIndex); } useKeyDown('ArrowDown', ArrowDown); @@ -201,6 +201,10 @@ export default function Menu() { }); }, [selected, menu, items]); + useEffect(() => { + setSelected(0); + }, [menu]); + useEffect(() => { debugData([ { diff --git a/web/src/lib/index.ts b/web/src/lib/index.ts index 09fc1ad..8ec8308 100644 --- a/web/src/lib/index.ts +++ b/web/src/lib/index.ts @@ -34,15 +34,3 @@ export function debugData
(events: DebugEvent
[], timer = 1000) {
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-
-export function findLastIndex