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();
}
}