mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-18 15:36:02 +01:00
feat(web/radial): global menu pagination
adds a More... button when menu items are over PAGE_ITEMS
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"close": "Mbylle",
|
"close": "Mbylle",
|
||||||
"confirm": "Konfirmo"
|
"confirm": "Konfirmo",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"close": "Zavřít",
|
"close": "Zavřít",
|
||||||
"confirm": "Potvrdit"
|
"confirm": "Potvrdit",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"confirm": "Bestätigen"
|
"confirm": "Bestätigen",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"confirm": "Confirm"
|
"confirm": "Confirm",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
"confirm": "Confirmar"
|
"confirm": "Confirmar",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Tühista",
|
"cancel": "Tühista",
|
||||||
"close": "Sulge",
|
"close": "Sulge",
|
||||||
"confirm": "Kinnita"
|
"confirm": "Kinnita",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"confirm": "Confirmer"
|
"confirm": "Confirmer",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "ביטול",
|
"cancel": "ביטול",
|
||||||
"close": "סגירה",
|
"close": "סגירה",
|
||||||
"confirm": "אישור"
|
"confirm": "אישור",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Odustani",
|
"cancel": "Odustani",
|
||||||
"close": "Zatvori",
|
"close": "Zatvori",
|
||||||
"confirm": "Potvrdi"
|
"confirm": "Potvrdi",
|
||||||
|
"more": "Više..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"close": "Bezárás",
|
"close": "Bezárás",
|
||||||
"confirm": "Megerősít"
|
"confirm": "Megerősít",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Batal",
|
"cancel": "Batal",
|
||||||
"close": "Tutup",
|
"close": "Tutup",
|
||||||
"confirm": "Konfirmasi"
|
"confirm": "Konfirmasi",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"close": "Chiudere",
|
"close": "Chiudere",
|
||||||
"confirm": "Confermare"
|
"confirm": "Confermare",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Atšaukti",
|
"cancel": "Atšaukti",
|
||||||
"close": "Uždaryti",
|
"close": "Uždaryti",
|
||||||
"confirm": "Patvirtinti"
|
"confirm": "Patvirtinti",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"close": "Sluiten",
|
"close": "Sluiten",
|
||||||
"confirm": "Bevestig"
|
"confirm": "Bevestig",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"close": "Zamknij",
|
"close": "Zamknij",
|
||||||
"confirm": "Potwierdź"
|
"confirm": "Potwierdź",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Отменить",
|
"cancel": "Отменить",
|
||||||
"close": "Закрывать",
|
"close": "Закрывать",
|
||||||
"confirm": "Подтверждать"
|
"confirm": "Подтверждать",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Zrušiť",
|
"cancel": "Zrušiť",
|
||||||
"close": "Zavrieť",
|
"close": "Zavrieť",
|
||||||
"confirm": "Potvrdiť"
|
"confirm": "Potvrdiť",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"close": "Zapri",
|
"close": "Zapri",
|
||||||
"confirm": "Potrdi"
|
"confirm": "Potrdi",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "Avbryt",
|
"cancel": "Avbryt",
|
||||||
"close": "Stäng",
|
"close": "Stäng",
|
||||||
"confirm": "Acceptera"
|
"confirm": "Acceptera",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"confirm": "确认"
|
"confirm": "确认",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"ui": {
|
"ui": {
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"confirm": "確認"
|
"confirm": "確認",
|
||||||
|
"more": "More..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Box, createStyles } from '@mantine/core';
|
import { Box, createStyles } from '@mantine/core';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
import { useNuiEvent } from '../../../hooks/useNuiEvent';
|
||||||
import { fetchNui } from '../../../utils/fetchNui';
|
import { fetchNui } from '../../../utils/fetchNui';
|
||||||
import ScaleFade from '../../../transitions/ScaleFade';
|
import ScaleFade from '../../../transitions/ScaleFade';
|
||||||
import type { RadialMenuItem } from '../../../typings';
|
import type { RadialMenuItem } from '../../../typings';
|
||||||
|
import { useLocales } from '../../../providers/LocaleProvider';
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
const useStyles = createStyles((theme) => ({
|
||||||
wrapper: {
|
wrapper: {
|
||||||
@@ -51,23 +52,41 @@ const useStyles = createStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// PAGE_ITEMS + 1 = More... button
|
||||||
|
const PAGE_ITEMS = 7;
|
||||||
|
|
||||||
const degToRad = (deg: number) => deg * (Math.PI / 180);
|
const degToRad = (deg: number) => deg * (Math.PI / 180);
|
||||||
|
|
||||||
const RadialMenu: React.FC = () => {
|
const RadialMenu: React.FC = () => {
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
const { locale } = useLocales();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean }>({
|
const [menuItems, setMenuItems] = useState<RadialMenuItem[]>([]);
|
||||||
|
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean; page: number }>({
|
||||||
items: [],
|
items: [],
|
||||||
sub: false,
|
sub: false,
|
||||||
|
page: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changePage = async (increment?: boolean) => {
|
||||||
|
setVisible(false);
|
||||||
|
// May cause issues if user toggles off the menu while in transition?
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
setVisible(true);
|
||||||
|
setMenu({ ...menu, page: increment ? menu.page + 1 : menu.page - 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (menu.items.length < PAGE_ITEMS) return setMenuItems(menu.items);
|
||||||
|
const items = menu.items.slice(PAGE_ITEMS * (menu.page - 1), PAGE_ITEMS * menu.page);
|
||||||
|
PAGE_ITEMS * menu.page < menu.items.length &&
|
||||||
|
items.push({ icon: 'ellipsis-h', label: locale.ui.more, isMore: true });
|
||||||
|
setMenuItems(items);
|
||||||
|
}, [menu.items, menu.page]);
|
||||||
|
|
||||||
useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean } | false) => {
|
useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean } | false) => {
|
||||||
if (!data) return setVisible(false);
|
if (!data) return setVisible(false);
|
||||||
// if (visible) {
|
setMenu({ ...data, page: 1 });
|
||||||
// setVisible(false);
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
// }
|
|
||||||
setMenu(data);
|
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,16 +96,22 @@ const RadialMenu: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box className={classes.wrapper} onContextMenu={() => menu.sub && fetchNui('radialBack')}>
|
<Box
|
||||||
|
className={classes.wrapper}
|
||||||
|
onContextMenu={async () => {
|
||||||
|
if (menu.page > 1) await changePage();
|
||||||
|
else if (menu.sub) fetchNui('radialBack');
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ScaleFade visible={visible}>
|
<ScaleFade visible={visible}>
|
||||||
<svg width="350px" height="350px" transform="rotate(90)">
|
<svg width="350px" height="350px" transform="rotate(90)">
|
||||||
{/*Fixed issues with background circle extending the circle when there's less than 3 items*/}
|
{/*Fixed issues with background circle extending the circle when there's less than 3 items*/}
|
||||||
<g transform="translate(175, 175)">
|
<g transform="translate(175, 175)">
|
||||||
<circle r={175} className={classes.backgroundCircle} />
|
<circle r={175} className={classes.backgroundCircle} />
|
||||||
</g>
|
</g>
|
||||||
{menu.items.map((item, index) => {
|
{menuItems.map((item, index) => {
|
||||||
// Always draw full circle to avoid elipse circles with 2 or less items
|
// Always draw full circle to avoid elipse circles with 2 or less items
|
||||||
const pieAngle = 360 / (menu.items.length < 3 ? 3 : menu.items.length);
|
const pieAngle = 360 / (menuItems.length < 3 ? 3 : menuItems.length);
|
||||||
const angle = degToRad(pieAngle / 2 + 90);
|
const angle = degToRad(pieAngle / 2 + 90);
|
||||||
const gap = 0;
|
const gap = 0;
|
||||||
const radius = 175 * 0.65 - gap;
|
const radius = 175 * 0.65 - gap;
|
||||||
@@ -100,7 +125,13 @@ const RadialMenu: React.FC = () => {
|
|||||||
<g
|
<g
|
||||||
transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`}
|
transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`}
|
||||||
className={classes.sector}
|
className={classes.sector}
|
||||||
onClick={() => fetchNui('radialClick', index)}
|
onClick={async () => {
|
||||||
|
const clickIndex = menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) + index;
|
||||||
|
if (!item.isMore) fetchNui('radialClick', clickIndex);
|
||||||
|
else {
|
||||||
|
await changePage(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${
|
d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${
|
||||||
@@ -126,11 +157,14 @@ const RadialMenu: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
<g
|
<g
|
||||||
transform={`translate(175, 175)`}
|
transform={`translate(175, 175)`}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (menu.sub) fetchNui('radialBack');
|
if (menu.page > 1) await changePage();
|
||||||
else {
|
else {
|
||||||
setVisible(false);
|
if (menu.sub) fetchNui('radialBack');
|
||||||
fetchNui('radialClose');
|
else {
|
||||||
|
setVisible(false);
|
||||||
|
fetchNui('radialClose');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -139,7 +173,7 @@ const RadialMenu: React.FC = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
<div className={classes.centerIconContainer}>
|
<div className={classes.centerIconContainer}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={!menu.sub ? 'xmark' : 'arrow-rotate-left'}
|
icon={!menu.sub && menu.page < 2 ? 'xmark' : 'arrow-rotate-left'}
|
||||||
fixedWidth
|
fixedWidth
|
||||||
className={classes.centerIcon}
|
className={classes.centerIcon}
|
||||||
color="#fff"
|
color="#fff"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ debugData([
|
|||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
|
more: 'More...',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -29,6 +30,7 @@ interface Locale {
|
|||||||
cancel: string;
|
cancel: string;
|
||||||
close: string;
|
close: string;
|
||||||
confirm: string;
|
confirm: string;
|
||||||
|
more: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ const LocaleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
|||||||
cancel: '',
|
cancel: '',
|
||||||
close: '',
|
close: '',
|
||||||
confirm: '',
|
confirm: '',
|
||||||
|
more: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
|||||||
export interface RadialMenuItem {
|
export interface RadialMenuItem {
|
||||||
icon: IconProp;
|
icon: IconProp;
|
||||||
label: string;
|
label: string;
|
||||||
|
isMore?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user