import { Component, ChangeDetectionStrategy, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, Renderer2, SimpleChanges, ViewChild, inject, } from '@angular/core'; import { CommonModule } from '@angular/common'; import type { Note } from '../../types'; import { ToastService } from '../../app/shared/toast/toast.service'; type NoteAction = | 'duplicate' | 'share' | 'fullscreen' | 'copy-link' | 'favorite' | 'info' | 'readonly' | 'delete'; @Component({ selector: 'app-note-context-menu', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, styles: [` :host { position: fixed; inset: 0; pointer-events: none; z-index: 9999; } .ctx { pointer-events: auto; min-width: 17.5rem; max-width: 21.25rem; border-radius: 1rem; padding: 0.5rem 0; box-shadow: 0 10px 30px rgba(0,0,0,.25); backdrop-filter: blur(6px); animation: fadeIn .12s ease-out; transform-origin: top left; user-select: none; /* Theme-aware background and border */ background: var(--card, #ffffff); border: 1px solid var(--border, #e5e7eb); color: var(--fg, #111827); z-index: 10000; } .item { display: flex; align-items: center; gap: 0.75rem; width: 100%; height: 2.25rem; text-align: left; padding: 0 1rem; border-radius: 0.5rem; border: none; background: transparent; cursor: pointer; font-size: 0.875rem; transition: background-color 0.08s ease; color: var(--text-main, #111827); } .item:hover { background: color-mix(in oklab, var(--surface-1, #f8fafc) 90%, black 0%); } .item:active { background: color-mix(in oklab, var(--surface-2, #eef2f7) 85%, black 0%); } .item.danger { color: var(--danger, #ef4444); } .item.warning { color: var(--warning, #f59e0b); } .item.disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } .sep { border-top: 1px solid var(--border, #e5e7eb); margin: 0.25rem 0; } .color-row { display: flex; align-items: center; justify-content: space-around; gap: 0.5rem; padding: 0.5rem; } .color-dot { width: 0.875rem; height: 0.875rem; border-radius: 9999px; cursor: pointer; transition: transform .08s ease, box-shadow .08s ease; border: 2px solid transparent; } .color-dot:hover { transform: scale(1.15); box-shadow: 0 0 0 2px color-mix(in oklab, var(--canvas, #ffffff) 70%, var(--fg, #111827) 15%); } .color-dot.active { box-shadow: 0 0 0 2px var(--fg, #111827); transform: scale(1.1); } .icon { width: 1.125rem; height: 1.125rem; flex-shrink: 0; } @keyframes fadeIn { from { opacity:0; transform: scale(.95);} to { opacity:1; transform: scale(1);} } `], template: ` `, }) export class NoteContextMenuComponent implements OnChanges, OnDestroy { /** Position demandée (pixels viewport) */ @Input() x = 0; @Input() y = 0; /** Contrôle d'affichage */ @Input() visible = false; /** Note concernée */ @Input() note: Note | null = null; /** Actions/retours */ @Output() action = new EventEmitter(); @Output() color = new EventEmitter(); @Output() closed = new EventEmitter(); /** Palette 8 couleurs + option pour effacer */ colors = ['#00AEEF', '#3B82F6', '#22C55E', '#F59E0B', '#EF4444', '#A855F7', '#8B5CF6', '#64748B']; /** Position corrigée (anti overflow) */ left = 0; top = 0; @ViewChild('menu') menuRef?: ElementRef; private removeResize?: () => void; private removeScroll?: () => void; private toastService = inject(ToastService); constructor(private r2: Renderer2, private host: ElementRef) { // listeners globaux qui ferment le menu this.removeResize = this.r2.listen('window', 'resize', () => this.reposition()); this.removeScroll = this.r2.listen('window', 'scroll', () => this.reposition()); } ngOnChanges(changes: SimpleChanges): void { if (changes['visible'] && this.visible) { // Immediately set to click position to avoid flashing at 0,0 this.left = this.x; this.top = this.y; // Then reposition for anti-overflow queueMicrotask(() => this.reposition()); } if ((changes['x'] || changes['y']) && this.visible) { queueMicrotask(() => this.reposition()); } } ngOnDestroy(): void { this.removeResize?.(); this.removeScroll?.(); } /** Ferme le menu */ close() { if (!this.visible) return; this.visible = false; this.closed.emit(); } emitAction(a: NoteAction) { // Check permissions before emitting if (a === 'duplicate' && !this.canDuplicate) { this.toastService.warning('Action non disponible en lecture seule'); return; } if (a === 'share' && !this.canShare) { this.toastService.warning('Partage non disponible pour cette note'); return; } if (a === 'readonly' && !this.canToggleReadOnly) { this.toastService.warning('Modification des permissions non disponible'); return; } if (a === 'delete' && !this.canDelete) { this.toastService.warning('Suppression non disponible pour cette note'); return; } this.action.emit(a); this.close(); } emitColor(c: string) { this.color.emit(c); this.close(); } /** Permissions calculées */ get canDuplicate(): boolean { return !this.note?.frontmatter?.readOnly; } get canShare(): boolean { // Vérifier si le partage public est activé dans la config // et si la note n'est pas privée return this.note?.frontmatter?.publish !== false && this.note?.frontmatter?.private !== true; } get canToggleReadOnly(): boolean { // Autorisé si on n'est pas en lecture seule globale return true; // Pour l'instant, on autorise toujours } get canDelete(): boolean { return true; // Pour l'instant, on autorise toujours } /** Corrige la position si le menu sortirait du viewport */ private reposition() { const el = this.menuRef?.nativeElement; if (!el) { this.left = this.x; this.top = this.y; return; } const menuRect = el.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; let left = this.x; let top = this.y; if (left + menuRect.width > vw - 8) left = Math.max(8, vw - menuRect.width - 8); if (top + menuRect.height > vh - 8) top = Math.max(8, vh - menuRect.height - 8); this.left = left; this.top = top; } /** Fermer avec ESC */ @HostListener('window:keydown', ['$event']) onKey(e: KeyboardEvent) { if (e.key === 'Escape') this.close(); } }