nitro-v36-with-renderer-v36
/
src /
components /
catalog /
views /
navigation /
CatalogNavigationItemView.tsx
CatalogNavigationItemView.tsx 6.5 Ko
Télécharger le fichier
import { FC, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
import { FaArrowsAlt, FaPlus, FaTrash } from 'react-icons/fa';
import { ICatalogNode, LocalizeText } from '../../../../api';
import { CssNativeText } from '../../../../common/native-text/CssNativeText';
import { parseCatalogTabLabel } from '../../useCatalogWindowWidth';
import { CatalogIconView } from '../catalog-icon/CatalogIconView';
import { CatalogNavigationRuntime } from './CatalogNavigationRuntime';
import { CatalogNavigationSetView } from './CatalogNavigationSetView';
export interface CatalogNavigationItemViewProps {
node: ICatalogNode;
runtime: CatalogNavigationRuntime;
child?: boolean;
}
export const CatalogNavigationItemView: FC<CatalogNavigationItemViewProps> = (props) => {
const { node = null, runtime, child = false } = props;
const { activateNode, adminMode, createSubpage, deletePage, reorderPage } = runtime;
const [isDragOver, setIsDragOver] = useState(false);
const dragRef = useRef<HTMLDivElement>(null);
// Strip only technical technical suffixes; labels such as
// "Flags (Wall)" or "Forest (Blue)" are meaningful catalog names.
const swfLabel = (node?.localization || '').replace(/\s*\((?:BC|Hot)\)\s*$/i, '').trim();
// The gameserver appends the page id ("Wisselkoers (3)") for staff with ACC_CATALOG_IDS. AIR shows the name
// only; the id stays in the row's tooltip.
const { name: navLabel, count: pageIdHint } = parseCatalogTabLabel(swfLabel);
const handleDragStart = useCallback(
(e: React.DragEvent) => {
if (!adminMode) return;
e.dataTransfer.setData('text/plain', JSON.stringify({ pageId: node.pageId, parentId: node.parent?.pageId ?? -1 }));
e.dataTransfer.effectAllowed = 'move';
},
[adminMode, node]
);
const handleDragOver = useCallback(
(e: React.DragEvent) => {
if (!adminMode) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setIsDragOver(true);
},
[adminMode]
);
const handleDragLeave = useCallback(() => {
setIsDragOver(false);
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
if (!adminMode) return;
e.preventDefault();
setIsDragOver(false);
try {
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
if (data.pageId && data.pageId !== node.pageId) {
// Drop onto a branch = reparent under this node
// Drop onto a leaf = reorder as sibling
const targetParentId = node.isBranch ? node.pageId : (node.parent?.pageId ?? -1);
const targetIndex = node.isBranch ? 0 : (node.parent?.children?.indexOf(node) ?? 0);
reorderPage(data.pageId, targetParentId, targetIndex);
}
} catch (err) {
// Invalid drag data
}
},
[adminMode, node, reorderPage]
);
useEffect(() => {
if (!node?.isActive || !dragRef.current?.scrollIntoView) return;
dragRef.current.scrollIntoView({ block: 'nearest' });
}, [node?.isActive]);
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
activateNode(node);
},
[activateNode, node]
);
return (
<div className={`octane-catalog-navigation-node ${child ? 'is-child' : ''}`}>
<div
ref={dragRef}
className={`octane-catalog-navigation-item group/nav ${node.isActive ? 'is-active' : ''} ${node.isBranch ? 'is-branch' : 'is-leaf'} ${node.isOpen ? 'is-open' : ''} ${isDragOver ? 'is-drag-over' : ''}`}
draggable={adminMode}
role="treeitem"
tabIndex={0}
aria-expanded={node.isBranch ? node.isOpen : undefined}
aria-level={(node.depth ?? 0) + 1}
aria-selected={node.isActive}
onClick={() => activateNode(node)}
onKeyDown={handleKeyDown}
onDragLeave={adminMode ? handleDragLeave : undefined}
onDragOver={adminMode ? handleDragOver : undefined}
onDragStart={adminMode ? handleDragStart : undefined}
onDrop={adminMode ? handleDrop : undefined}
>
{adminMode && (
<FaArrowsAlt className="octane-catalog-navigation-drag text-[7px] text-muted cursor-grab shrink-0 opacity-0 group-hover/nav:opacity-60" />
)}
<div className="octane-catalog-navigation-icon">
<CatalogIconView icon={node.iconId} />
</div>
<span
className="octane-catalog-navigation-label"
title={adminMode || pageIdHint !== null ? `Page ID: ${pageIdHint ?? node.pageId}` : undefined}
>
<CssNativeText colorKey={node.isActive} text={navLabel} textStyle={child ? 'u_bold_italic' : 'u_bold'} />
</span>
{adminMode && (
<div className="octane-catalog-navigation-admin flex items-center gap-1 opacity-0 group-hover/nav:opacity-100 transition-opacity">
<FaPlus
className="text-[8px] text-success hover:text-green-800"
title={LocalizeText('catalog.admin.create.subpage')}
onClick={(e) => {
e.stopPropagation();
createSubpage(node);
}}
/>
<FaTrash
className="text-[8px] text-danger hover:text-red-700"
title={LocalizeText('catalog.admin.delete.page')}
onClick={(e) => {
e.stopPropagation();
deletePage(node);
}}
/>
</div>
)}
{node.isBranch && <span aria-hidden="true" className={`octane-catalog-navigation-caret${node.isOpen ? ' is-open' : ''}`} />}
</div>
{node.isOpen && node.isBranch && <CatalogNavigationSetView child={true} node={node} runtime={runtime} />}
</div>
);
};
