AltitudeWeb/frontend/src/app/pages/altitude/chat/mini-message/obfuscated.directive.ts

66 lines
1.9 KiB
TypeScript

import { Directive, ElementRef, Input, OnChanges, OnDestroy } from '@angular/core';
const OBFUSCATION_CHARS =
'!@#$%^&*()_+-=[]{}|;:,.<>?/~`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
/**
* Apply as `[mmObfuscated]="style.obfuscated"` on an element whose textContent is the
* text to obfuscate. While active, cycles every character (except spaces) through random
* glyphs, mimicking Minecraft's "magic" formatting. The real text is preserved in an
* aria-label so it stays accessible to screen readers, and is restored to the DOM when
* obfuscation is turned off.
*/
@Directive({
selector: '[mmObfuscated]',
standalone: true,
})
export class ObfuscatedDirective implements OnChanges, OnDestroy {
@Input('mmObfuscated') active = false;
private original: string | null = null;
private intervalId: ReturnType<typeof setInterval> | undefined;
constructor(private readonly el: ElementRef<HTMLElement>) {}
ngOnChanges(): void {
if (this.original === null) {
this.original = this.el.nativeElement.textContent ?? '';
this.el.nativeElement.setAttribute('aria-label', this.original);
}
this.sync();
}
ngOnDestroy(): void {
this.stop();
}
private sync(): void {
if (this.active && !this.intervalId) {
this.start();
} else if (!this.active && this.intervalId) {
this.stop();
}
}
private start(): void {
this.intervalId = setInterval(() => {
const text = this.original ?? '';
const scrambled = text
.split('')
.map((ch) => (ch === ' ' ? ' ' : OBFUSCATION_CHARS[Math.floor(Math.random() * OBFUSCATION_CHARS.length)]))
.join('');
this.el.nativeElement.textContent = scrambled;
}, 50);
}
private stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
if (this.original !== null) {
this.el.nativeElement.textContent = this.original;
}
}
}