| |
| |
| |
| |
| import * as d3 from 'd3'; |
| import { tr } from '../lang/i18n-lite'; |
|
|
| export type PathNavigator = { |
| update: (path: string) => void; |
| }; |
|
|
| export function createPathNavigator( |
| container: d3.Selection<HTMLElement, any, any, any>, |
| currentPath: string, |
| onPathChange: (path: string) => void, |
| onCreateFolder?: () => void |
| ): PathNavigator { |
| const navWrapper = container.append('div') |
| .attr('class', 'demo-path-nav-wrapper') |
| .style('display', 'flex') |
| .style('align-items', 'center') |
| .style('justify-content', 'space-between') |
| .style('gap', '10px'); |
|
|
| const navContainer = navWrapper.append('div') |
| .attr('class', 'demo-path-navigator') |
| .style('flex', '1') |
| .style('font-size', '12px') |
| .style('color', 'var(--text-color)'); |
|
|
| |
| if (onCreateFolder) { |
| const createBtn = navWrapper.append('button') |
| .attr('class', 'refresh-btn') |
| .attr('title', tr('New folder')) |
| .style('flex-shrink', '0') |
| .text('+') |
| .on('click', function() { |
| onCreateFolder(); |
| }); |
| } |
|
|
| const update = (path: string) => { |
| navContainer.selectAll('*').remove(); |
|
|
| |
| const pathSegments: Array<{ name: string; path: string; isRoot?: boolean }> = []; |
| |
| |
| pathSegments.push({ name: '', path: '/', isRoot: true }); |
|
|
| if (path && path !== '/') { |
| |
| const segments = path.split('/').filter(s => s); |
| let currentFullPath = '/'; |
| |
| segments.forEach(segment => { |
| |
| currentFullPath = currentFullPath === '/' ? `/${segment}` : `${currentFullPath}/${segment}`; |
| pathSegments.push({ |
| name: decodeURIComponent(segment), |
| path: currentFullPath |
| }); |
| }); |
| } |
|
|
| |
| const pathItems = navContainer.selectAll('.path-segment') |
| .data(pathSegments) |
| .join('span') |
| .attr('class', 'path-segment') |
| .style('cursor', 'pointer') |
| .style('color', 'var(--text-color)') |
| .style('opacity', '0.7') |
| .style('transition', 'opacity 0.2s') |
| .on('mouseenter', function() { |
| d3.select(this).style('opacity', '1'); |
| }) |
| .on('mouseleave', function() { |
| d3.select(this).style('opacity', '0.7'); |
| }) |
| .on('click', function(_, d) { |
| onPathChange(d.path); |
| }) |
| .each(function(d) { |
| const seg = d3.select(this); |
| if (d.isRoot) { |
| seg |
| .attr('title', tr('/(Root)')) |
| .attr('aria-label', tr('/(Root)')) |
| .text('⌂'); |
| return; |
| } |
| seg.text(d.name); |
| }); |
|
|
| |
| const separators = navContainer.selectAll('.path-separator') |
| .data(pathSegments.slice(0, -1)) |
| .join('span') |
| .attr('class', 'path-separator') |
| .style('margin', '0 6px') |
| .style('opacity', '0.5') |
| .text(' > '); |
|
|
| |
| pathItems.each(function(d, i) { |
| if (i > 0) { |
| const separator = separators.filter((_, idx) => idx === i - 1); |
| const separatorNode = separator.node() as HTMLElement | null; |
| const thisNode = this as HTMLElement; |
| if (separatorNode && thisNode.parentNode) { |
| thisNode.parentNode.insertBefore(separatorNode, thisNode); |
| } |
| } |
| }); |
| }; |
|
|
| |
| update(currentPath); |
|
|
| return { update }; |
| } |
|
|
|
|