import { Component, EventEmitter, Output, computed, signal, effect, inject } from '@angular/core';
import { input } from '@angular/core';
import { CommonModule } from '@angular/common';
import type { Note } from '../../../types';
import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrollable-overlay.directive';
import { TagFilterStore } from '../../core/stores/tag-filter.store';
@Component({
selector: 'app-notes-list',
standalone: true,
imports: [CommonModule, ScrollableOverlayDirective],
template: `
`,
styles: [`
:host {
display: block;
height: 100%;
min-height: 0; /* critical for nested flex scrolling */
}
/* Smooth, bounded vertical scrolling only on the list area */
.list-scroll {
overscroll-behavior: contain; /* prevent parent scroll chaining */
-webkit-overflow-scrolling: touch; /* momentum scrolling on iOS */
scroll-behavior: smooth; /* smooth programmatic scrolls */
scrollbar-gutter: stable both-edges; /* avoid layout shift when scrollbar shows */
max-height: 100%; /* cap to available space within the central section */
contain: content; /* small perf win for large lists */
}
`]
})
export class NotesListComponent {
notes = input([]);
folderFilter = input(null); // like "folder/subfolder"
query = input('');
tagFilter = input(null);
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
@Output() openNote = new EventEmitter();
@Output() queryChange = new EventEmitter();
private store = inject(TagFilterStore);
private q = signal('');
activeTag = signal(null);
private syncQuery = effect(() => {
this.q.set(this.query() || '');
});
private syncTagFromStore = effect(() => {
// Prefer explicit input; otherwise, take store value
const inputTag = this.tagFilter();
if (inputTag !== null && inputTag !== undefined) {
this.activeTag.set(inputTag || null);
return;
}
this.activeTag.set(this.store.get());
});
filtered = computed(() => {
const q = (this.q() || '').toLowerCase().trim();
const folder = (this.folderFilter() || '').toLowerCase().replace(/^\/+|\/+$/g, '');
const tag = (this.activeTag() || '').toLowerCase();
const quickLink = this.quickLinkFilter();
let list = this.notes();
if (folder) {
if (folder === '.trash') {
// All files anywhere under .trash (including subfolders)
list = list.filter(n => {
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
return filePath.startsWith('.trash/') || filePath.includes('/.trash/');
});
} else {
list = list.filter(n => {
const originalPath = (n.originalPath || '').toLowerCase().replace(/^\/+|\/+$/g, '');
return originalPath === folder || originalPath.startsWith(folder + '/');
});
}
}
if (tag) {
list = list.filter(n => Array.isArray(n.tags) && n.tags.some(t => (t || '').toLowerCase() === tag));
}
// Apply Quick Link filter (favoris, template, task)
if (quickLink) {
list = list.filter(n => {
const frontmatter = n.frontmatter || {};
return frontmatter[quickLink] === true;
});
}
// Apply query if present
if (q) {
list = list.filter(n => {
const title = (n.title || '').toLowerCase();
const filePath = (n.filePath || '').toLowerCase();
return title.includes(q) || filePath.includes(q);
});
}
// Sort by most recent first (mtime desc; fallback updatedAt/createdAt)
const parseDate = (s?: string) => (s ? Date.parse(s) : 0) || 0;
const score = (n: Note) => n.mtime || parseDate(n.updatedAt) || parseDate(n.createdAt) || 0;
return [...list].sort((a, b) => (score(b) - score(a)));
});
onQuery(v: string) {
this.q.set(v);
this.queryChange.emit(v);
}
clearTagFilter(): void {
// Clear both local input state and store
this.activeTag.set(null);
if (this.tagFilter() == null) {
this.store.set(null);
}
}
}