import { AfterViewInit, Component, ElementRef, Input, OnDestroy, Output, EventEmitter, ViewContainerRef, inject, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { debounceTime, Subject } from 'rxjs'; import { splitPathKeepFilename } from '../../../../shared/utils/path'; import { TagManagerComponent } from '../../../../shared/tags/tag-manager/tag-manager.component'; import { Overlay, OverlayRef } from '@angular/cdk/overlay'; import { ComponentPortal } from '@angular/cdk/portal'; import { PropertiesPopoverComponent } from '../properties-popover/properties-popover.component'; import { FrontmatterPropertiesService } from '../../shared/frontmatter-properties.service'; import { VaultService } from '../../../../../services/vault.service'; @Component({ selector: 'app-note-header', standalone: true, imports: [CommonModule, TagManagerComponent], templateUrl: './note-header.component.html', styleUrls: ['./note-header.component.scss'] }) export class NoteHeaderComponent implements AfterViewInit, OnDestroy, OnChanges { @Input() fullPath = ''; @Input() noteId = ''; @Input() tags: string[] = []; @Output() openDirectory = new EventEmitter(); @Output() copyRequested = new EventEmitter(); @Output() tagsChange = new EventEmitter(); @Output() tagSelected = new EventEmitter(); pathParts: { prefix: string; filename: string } = { prefix: '', filename: '' }; private ro?: ResizeObserver; private resize$ = new Subject(); // Properties popover popoverOpen = false; private overlayRef?: OverlayRef; private closeTimer?: any; private overlay = inject(Overlay); private vcr = inject(ViewContainerRef); private frontmatterService = inject(FrontmatterPropertiesService); private vaultService = inject(VaultService); constructor(private host: ElementRef) {} ngOnChanges(changes: SimpleChanges): void { if (changes['fullPath']) { this.pathParts = splitPathKeepFilename(this.fullPath); // Also update the path display if component is already initialized if (this.ro) { this.fitPath(); } } } ngAfterViewInit(): void { this.pathParts = splitPathKeepFilename(this.fullPath); this.ro = new ResizeObserver(() => this.resize$.next()); this.ro.observe(this.host.nativeElement); this.resize$.pipe(debounceTime(50)).subscribe(() => { this.applyProgressiveCollapse(); this.fitPath(); }); queueMicrotask(() => { this.applyProgressiveCollapse(); this.fitPath(); }); } ngOnDestroy(): void { this.ro?.disconnect(); this.closePopover(); } private applyProgressiveCollapse(): void { const root = this.host.nativeElement; const extras = root.querySelector('.note-header__extras') as HTMLElement | null; if (!extras) return; extras.querySelectorAll('[data-collapse-priority]').forEach(el => { el.style.display = ''; }); const overflowing = () => root.scrollWidth > root.clientWidth + 2; const candidates = Array.from( extras.querySelectorAll('[data-collapse-priority]') ).sort((a, b) => (parseInt(b.dataset.collapsePriority || '0', 10)) - (parseInt(a.dataset.collapsePriority || '0', 10)) ); for (const el of candidates) { if (!overflowing()) break; el.style.display = 'none'; } } private fitPath(): void { const root = this.host.nativeElement; const prefix = root.querySelector('.path-prefix') as HTMLElement | null; const filename = root.querySelector('.path-filename') as HTMLElement | null; if (!prefix || !filename) return; const base = splitPathKeepFilename(this.fullPath); let prefixText = base.prefix; let fileText = base.filename; prefix.textContent = prefixText; filename.textContent = fileText; const fits = () => root.scrollWidth <= root.clientWidth + 2; while (!fits() && prefixText.length > 0) { const slashIdxs = ['/', '\\'] .map(s => prefixText.indexOf(s)) .filter(i => i >= 0); const slashIdx = slashIdxs.length ? Math.min(...slashIdxs) : -1; if (slashIdx >= 0) { prefixText = prefixText.slice(slashIdx + 1); } else { prefixText = prefixText.slice(2); } const cleaned = prefixText.replace(/^[/\\]+/, ''); prefix.textContent = cleaned ? `…/${cleaned}` : ''; if (fits()) return; } if (!fits()) { const { head, ext } = splitName(fileText); let trimmed = head; while (!fits() && trimmed.length > 4) { trimmed = trimmed.slice(0, trimmed.length - 2); filename.textContent = trimmed + '…' + ext; } } function splitName(name: string) { const dot = name.lastIndexOf('.'); if (dot <= 0) return { head: name, ext: '' }; return { head: name.slice(0, dot), ext: name.slice(dot) }; } } onPathClick(): void { this.openDirectory.emit(); } onPathContextMenu(event: MouseEvent): void { event.preventDefault(); this.copyRequested.emit(); } openPopover(origin: HTMLElement): void { clearTimeout(this.closeTimer); if (this.overlayRef && this.popoverOpen) return; const positionStrategy = this.overlay.position() .flexibleConnectedTo(origin) .withPositions([ { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: 8 }, { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 }, ]) .withPush(true); this.overlayRef = this.overlay.create({ positionStrategy, hasBackdrop: false, scrollStrategy: this.overlay.scrollStrategies.reposition(), }); const portal = new ComponentPortal(PropertiesPopoverComponent, this.vcr); const compRef = this.overlayRef.attach(portal); // Get note and properties const note = this.vaultService.getNoteById(this.noteId); const props = this.frontmatterService.get(note); compRef.instance.props = props; compRef.instance.noteId = this.noteId; compRef.instance.requestClose.subscribe(() => this.scheduleClose()); compRef.instance.cancelClose.subscribe(() => clearTimeout(this.closeTimer)); this.overlayRef.outsidePointerEvents().subscribe(() => this.closePopover()); this.overlayRef.detachments().subscribe(() => { this.popoverOpen = false; }); this.popoverOpen = true; } scheduleClose(): void { clearTimeout(this.closeTimer); this.closeTimer = setTimeout(() => this.closePopover(), 150); } togglePopover(origin: HTMLElement): void { if (this.popoverOpen) { this.closePopover(); } else { this.openPopover(origin); } } closePopover(): void { clearTimeout(this.closeTimer); if (this.overlayRef) { this.overlayRef.dispose(); this.overlayRef = undefined; } this.popoverOpen = false; } }