146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
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: `
|
|
<div class="h-full flex flex-col">
|
|
<div class="p-2 border-b border-gray-200 dark:border-gray-800 space-y-2">
|
|
<div *ngIf="activeTag() as t" class="flex items-center gap-2 text-xs">
|
|
<span class="inline-flex items-center gap-1 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 px-2 py-1">
|
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
|
|
Filtre: #{{ t }}
|
|
</span>
|
|
<button type="button" (click)="clearTagFilter()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-slate-200/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
|
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
</button>
|
|
</div>
|
|
<input type="text"
|
|
[value]="query()"
|
|
(input)="onQuery($any($event.target).value)"
|
|
placeholder="Rechercher..."
|
|
class="w-full rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" />
|
|
</div>
|
|
<div class="flex-1 min-h-0 overflow-y-auto list-scroll" appScrollableOverlay>
|
|
<ul class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
<li *ngFor="let n of filtered()" class="p-3 hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer" (click)="openNote.emit(n.id)">
|
|
<div class="text-sm font-semibold truncate">{{ n.title }}</div>
|
|
<div class="text-xs text-gray-500 truncate">{{ n.filePath }}</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`,
|
|
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<Note[]>([]);
|
|
folderFilter = input<string | null>(null); // like "folder/subfolder"
|
|
query = input<string>('');
|
|
tagFilter = input<string | null>(null);
|
|
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
|
|
|
@Output() openNote = new EventEmitter<string>();
|
|
@Output() queryChange = new EventEmitter<string>();
|
|
|
|
private store = inject(TagFilterStore);
|
|
private q = signal('');
|
|
activeTag = signal<string | null>(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);
|
|
}
|
|
}
|
|
}
|