mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
264 lines
7.0 KiB
TypeScript
264 lines
7.0 KiB
TypeScript
import React, { useId } from 'react';
|
|
import Link from 'next/link';
|
|
import { Icon } from '@iconify/react';
|
|
import { cn, findMenuPath } from '@/lib/helper';
|
|
import { Size } from '@/types/theme';
|
|
import Button from '@/components/Button';
|
|
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
|
|
|
interface BreadcrumbItem {
|
|
label: string;
|
|
href?: string;
|
|
icon?: React.ReactNode;
|
|
isActive?: boolean;
|
|
isDisabled?: boolean;
|
|
}
|
|
|
|
interface BreadcrumbsProps extends React.HTMLAttributes<HTMLElement> {
|
|
items: BreadcrumbItem[];
|
|
size?: Size;
|
|
maxVisibleItems?: number;
|
|
showEllipsisDropdown?: boolean;
|
|
}
|
|
|
|
export function buildBreadcrumbs(pathname: string): BreadcrumbItem[] {
|
|
const menuPath = findMenuPath(MAIN_DRAWER_LINKS, pathname);
|
|
|
|
if (!menuPath) return [];
|
|
|
|
return menuPath.map((menu, index) => {
|
|
const isLast = index === menuPath.length - 1;
|
|
|
|
return {
|
|
label: menu.text,
|
|
href: isLast ? menu.link : undefined,
|
|
isActive: isLast,
|
|
icon: menu.icon ? (
|
|
<Icon icon={menu.icon} width={16} height={16} />
|
|
) : undefined,
|
|
};
|
|
});
|
|
}
|
|
|
|
const EllipsisDropdown = ({
|
|
hiddenItems,
|
|
}: {
|
|
hiddenItems: BreadcrumbItem[];
|
|
}) => {
|
|
const dropdownId = useId();
|
|
const anchorId = useId();
|
|
|
|
return (
|
|
<li>
|
|
{/* Ellipsis Button */}
|
|
<Button
|
|
popoverTarget={dropdownId}
|
|
variant='ghost'
|
|
color='none'
|
|
style={
|
|
{
|
|
anchorName: `--breadcrumb-ellipsis-anchor-${anchorId}`,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<Icon icon='material-symbols:more-horiz' width={16} height={16} />
|
|
</Button>
|
|
|
|
{/* Dropdown Menu using popover API */}
|
|
<ul
|
|
className='dropdown menu rounded-box bg-base-100 border border-base-300 shadow-lg z-[9999] [&_a:hover]:no-underline [&_a:focus]:no-underline [&&]:no-underline [&&_a]:no-underline [&&]:hover:no-underline [&&]:flex [&&]:items-start [&&]:justify-start w-max'
|
|
popover='auto'
|
|
id={dropdownId}
|
|
style={
|
|
{
|
|
positionAnchor: `--breadcrumb-ellipsis-anchor-${anchorId}`,
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
{hiddenItems.map((item, index) => {
|
|
const itemStyles = cn(
|
|
'[&]:flex [&]:items-center [&]:justify-start py-1 text-sm',
|
|
// Disabled state
|
|
item.isDisabled && 'text-base-content/40 opacity-50',
|
|
// Active/Last state
|
|
(item.isActive || item.isDisabled) && 'text-primary',
|
|
// Regular clickable state
|
|
!item.isDisabled && 'text-base-content/50'
|
|
);
|
|
|
|
const itemContent = (
|
|
<div className={itemStyles}>
|
|
{item.icon && (
|
|
<span className='inline-flex mr-2'>{item.icon}</span>
|
|
)}
|
|
{item.label}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<li
|
|
key={`ellipsis-${index}`}
|
|
className='[&&]:text-left [&&]:block w-full'
|
|
>
|
|
{item.href && !item.isDisabled ? (
|
|
<Link
|
|
href={item.href}
|
|
className='block !no-underline [&&]:text-left w-full'
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{itemContent}
|
|
</Link>
|
|
) : (
|
|
<div className='block !no-underline [&&]:cursor-default [&&]:hover:cursor-default [&&]:hover:bg-base-100 [&&]:text-left'>
|
|
{itemContent}
|
|
</div>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const Breadcrumb = ({
|
|
items,
|
|
size = 'md',
|
|
maxVisibleItems = 3,
|
|
showEllipsisDropdown = true,
|
|
className,
|
|
...props
|
|
}: BreadcrumbsProps) => {
|
|
const sizeClasses = {
|
|
xs: 'text-xs',
|
|
sm: 'text-sm',
|
|
md: 'text-base',
|
|
lg: 'text-lg',
|
|
xl: 'text-xl',
|
|
};
|
|
|
|
const getItemStyles = (
|
|
item: BreadcrumbItem,
|
|
position: 'first' | 'middle' | 'last' = 'middle'
|
|
) => {
|
|
const baseClasses = 'inline-flex items-center gap-2';
|
|
|
|
// Disabled state
|
|
if (item.isDisabled) {
|
|
return `${baseClasses} text-base-content/40 !cursor-default opacity-50 hover:!no-underline`;
|
|
}
|
|
|
|
// Active/Last state (no underline)
|
|
if (item.isActive || position === 'last') {
|
|
return `${baseClasses} text-primary !cursor-pointer hover:!no-underline`;
|
|
}
|
|
|
|
// Regular clickable state
|
|
return `${baseClasses} text-base-content/60`;
|
|
};
|
|
|
|
const renderItem = (
|
|
item: BreadcrumbItem,
|
|
position: 'first' | 'middle' | 'last' = 'middle'
|
|
) => {
|
|
const styles = getItemStyles(item, position);
|
|
|
|
// Disabled items
|
|
if (item.isDisabled) {
|
|
return (
|
|
<span className={styles}>
|
|
{item.icon && item.icon}
|
|
{item.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// Active/Last items
|
|
if (item.isActive || position === 'last') {
|
|
if (item.href) {
|
|
return (
|
|
<Link href={item.href} className={styles}>
|
|
{item.icon && (
|
|
<span className='inline-flex gap-2'>{item.icon}</span>
|
|
)}
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
}
|
|
return (
|
|
<span className={styles}>
|
|
{item.icon && item.icon}
|
|
{item.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// Regular items
|
|
if (item.href) {
|
|
return (
|
|
<Link href={item.href} className={styles}>
|
|
{item.icon && <span className='inline-flex gap-2'>{item.icon}</span>}
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span className={styles}>
|
|
{item.icon && item.icon}
|
|
{item.label}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const renderBreadcrumbList = () => {
|
|
// Show all items if within limit
|
|
if (items.length <= maxVisibleItems) {
|
|
return items.map((item, index) => {
|
|
const position =
|
|
index === 0
|
|
? 'first'
|
|
: index === items.length - 1
|
|
? 'last'
|
|
: 'middle';
|
|
return <li key={index}>{renderItem(item, position)}</li>;
|
|
});
|
|
}
|
|
|
|
// Collapsed items indexing when exceeding limit
|
|
const firstItem = items[0];
|
|
const lastItem = items[items.length - 1];
|
|
const visibleMiddleItems = items.slice(1, -1).slice(-(maxVisibleItems - 2));
|
|
const hiddenItems = items.slice(1, -1).slice(0, -(maxVisibleItems - 2));
|
|
const showEllipsis = showEllipsisDropdown && hiddenItems.length > 0;
|
|
|
|
return (
|
|
<>
|
|
<li>{renderItem(firstItem, 'first')}</li>
|
|
|
|
{/* Ellipsis for hidden items with dropdown */}
|
|
{showEllipsis && <EllipsisDropdown hiddenItems={hiddenItems} />}
|
|
|
|
{/* Middle items */}
|
|
{visibleMiddleItems.map((item, index) => (
|
|
<li key={`middle-${index}`}>{renderItem(item, 'middle')}</li>
|
|
))}
|
|
|
|
<li>{renderItem(lastItem, 'last')}</li>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<nav
|
|
aria-label='Breadcrumb'
|
|
className={cn('breadcrumbs', sizeClasses[size], className)}
|
|
{...props}
|
|
>
|
|
<ul className='text-sm'>{renderBreadcrumbList()}</ul>
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
export default Breadcrumb;
|