mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
210 lines
7.9 KiB
TypeScript
210 lines
7.9 KiB
TypeScript
import { Directive, ElementRef, HostListener } from '@angular/core';
|
|
|
|
/**
|
|
* Directive that ensures only positive integers can be entered into number input fields.
|
|
*
|
|
* This directive provides three layers of protection:
|
|
* 1. Keyboard input blocking - prevents invalid characters from being typed
|
|
* 2. Paste sanitization - cleans pasted content to extract only positive integers
|
|
* 3. General input validation - catches any other input methods (drag & drop, autofill, etc.)
|
|
*
|
|
* **Blocked characters:** `.`, `,`, `-`, `+`, `e`, `E`, and all other non-digit characters
|
|
*
|
|
* **Examples:**
|
|
* - Typing `1-2-3` → only `123` appears (minus signs blocked)
|
|
* - Pasting `-100` → becomes `100` (negative sign removed)
|
|
* - Pasting `1.000` → becomes `1000` (decimal point removed)
|
|
* - Pasting `1,58` → becomes `158` (comma removed)
|
|
* - Typing `007` → becomes `7` (leading zeros removed)
|
|
*
|
|
* **Important Notes:**
|
|
* - This directive removes ALL non-digit characters, so `1.58` becomes `158`, not `1` or `2`
|
|
* - Leading zeros are automatically removed
|
|
* - A single `0` input results in an empty field
|
|
* - Works seamlessly with Angular forms (ngModel, formControl)
|
|
*
|
|
* @example
|
|
* ```html
|
|
* <!-- Basic usage -->
|
|
* <input type="number" positiveIntegerInput />
|
|
*
|
|
* <!-- With ngModel -->
|
|
* <input type="number" positiveIntegerInput [(ngModel)]="points" />
|
|
*
|
|
* <!-- With form control -->
|
|
* <input type="number" positiveIntegerInput [formControl]="pointsControl" />
|
|
* ```
|
|
*
|
|
* @see {@link https://angular.dev/guide/directives} Angular Directives Documentation
|
|
*/
|
|
@Directive({
|
|
selector: 'input[type="number"][positiveIntegerInput]',
|
|
standalone: true,
|
|
})
|
|
export class PositiveIntegerInputDirective {
|
|
/**
|
|
* Set of characters that should be blocked during keyboard input.
|
|
* These characters are common in numeric input but not allowed for positive integers.
|
|
*/
|
|
private readonly blockedKeys = new Set(['.', ',', '-', '+', 'e', 'E']);
|
|
|
|
/**
|
|
* Injects the ElementRef to get direct access to the native input element.
|
|
* This is needed for the paste and input handlers to manipulate the input value.
|
|
*
|
|
* @param elementRef Reference to the host input element
|
|
*/
|
|
constructor(private elementRef: ElementRef<HTMLInputElement>) {}
|
|
|
|
/**
|
|
* Handles keyboard input events to prevent invalid characters from being entered.
|
|
*
|
|
* This is the first line of defense - it blocks keys before they can affect the input value.
|
|
* Blocked keys include: `.`, `,`, `-`, `+`, `e`, `E`
|
|
*
|
|
* Navigation keys (arrows, backspace, delete, tab, etc.) are NOT blocked,
|
|
* allowing normal text editing operations.
|
|
*
|
|
* @example
|
|
* User presses `-` → Event prevented, nothing happens
|
|
* User presses `5` → Allowed, `5` appears in the input
|
|
* User presses `.` → Event prevented, nothing happens
|
|
*
|
|
* @param event The keyboard event to evaluate
|
|
*/
|
|
@HostListener('keydown', ['$event'])
|
|
onKeyDown(event: KeyboardEvent): void {
|
|
// Check if the pressed key is in our blocked list
|
|
if (this.blockedKeys.has(event.key)) {
|
|
// Prevent the key from affecting the input
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles paste events to sanitize pasted content before it enters the input.
|
|
*
|
|
* This is the second line of defense - it prevents users from bypassing keyboard
|
|
* restrictions by pasting invalid content (e.g., from Excel, calculators, or text editors).
|
|
*
|
|
* The handler:
|
|
* 1. Prevents the default paste behavior
|
|
* 2. Extracts text from the clipboard
|
|
* 3. Sanitizes it to contain only positive integers (removes all non-digits)
|
|
* 4. Sets the sanitized value in the input
|
|
* 5. Triggers an input event so Angular forms (ngModel, formControl) detect the change
|
|
*
|
|
* @example
|
|
* User pastes `-100` → Input value becomes `100`
|
|
* User pastes `1.000` → Input value becomes `1000`
|
|
* User pastes `1,58` → Input value becomes `158`
|
|
* User pastes `abc123xyz` → Input value becomes `123`
|
|
* User pastes `00042` → Input value becomes `42`
|
|
*
|
|
* @param event The clipboard event containing the pasted data
|
|
*/
|
|
@HostListener('paste', ['$event'])
|
|
onPaste(event: ClipboardEvent): void {
|
|
// Prevent the default paste behavior to control what gets inserted
|
|
event.preventDefault();
|
|
|
|
// Get the text from the clipboard
|
|
const pastedText = event.clipboardData?.getData('text');
|
|
if (!pastedText) {
|
|
return;
|
|
}
|
|
|
|
// Remove all non-digit characters and leading zeros
|
|
const sanitized = this.sanitizeInput(pastedText);
|
|
|
|
if (sanitized) {
|
|
// Insert the sanitized value into the input
|
|
const input = this.elementRef.nativeElement;
|
|
input.value = sanitized;
|
|
|
|
// Trigger input event to update Angular form binding (ngModel/formControl)
|
|
// The 'bubbles: true' ensures the event propagates up to Angular's listeners
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles input events to catch any other input methods that bypass keydown and paste.
|
|
*
|
|
* This is the third line of defense - a catch-all for:
|
|
* - Drag and drop operations
|
|
* - Browser autofill/autocomplete
|
|
* - Programmatic value changes
|
|
* - Right-click paste (on some browsers)
|
|
* - Voice input
|
|
* - IME (Input Method Editor) input
|
|
*
|
|
* If the current input value contains invalid characters, they are removed and
|
|
* the sanitized value is set back to the input.
|
|
*
|
|
* @example
|
|
* User drags `-50` from a text document → Input value becomes `50`
|
|
* Browser autofills `1.234` → Input value becomes `1234`
|
|
*
|
|
* @param event The input event triggered by any value change
|
|
*/
|
|
@HostListener('input', ['$event'])
|
|
onInput(event: Event): void {
|
|
const input = event.target as HTMLInputElement;
|
|
const value = input.value;
|
|
|
|
// Sanitize the current value (catches any other ways of setting invalid values)
|
|
const sanitized = this.sanitizeInput(value);
|
|
|
|
// Only update if the value actually changed after sanitization
|
|
// This prevents unnecessary updates and potential infinite loops
|
|
if (value !== sanitized) {
|
|
input.value = sanitized;
|
|
// Trigger input event again to ensure Angular picks up the sanitized value
|
|
// This is necessary because we're modifying the value programmatically
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitizes a string to contain only positive integers.
|
|
*
|
|
* This method performs two operations:
|
|
* 1. Removes all non-digit characters using regex `\D` (matches anything that's not 0-9)
|
|
* 2. Removes leading zeros to prevent issues with number parsing
|
|
*
|
|
* **Examples:**
|
|
* - `sanitizeInput('-100')` → `'100'`
|
|
* - `sanitizeInput('1.000')` → `'1000'`
|
|
* - `sanitizeInput('1,58')` → `'158'` (NOT `'1'` or `'2'`!)
|
|
* - `sanitizeInput('+42')` → `'42'`
|
|
* - `sanitizeInput('3.14e2')` → `'314'`
|
|
* - `sanitizeInput('abc123xyz')` → `'123'`
|
|
* - `sanitizeInput('00123')` → `'123'`
|
|
* - `sanitizeInput('007')` → `'7'`
|
|
* - `sanitizeInput('0')` → `''` (empty string)
|
|
* - `sanitizeInput('abc')` → `''` (no digits found)
|
|
*
|
|
* **Important:** This method does NOT round or parse decimals.
|
|
* It simply removes all non-digit characters, so `1.58` becomes `158`, not `1` or `2`.
|
|
*
|
|
* @param value The input string to sanitize
|
|
* @returns A string containing only digits, with leading zeros removed
|
|
*
|
|
* @private This method is for internal use only
|
|
*/
|
|
private sanitizeInput(value: string): string {
|
|
// Remove all non-digit characters (including -, +, ., ,, e, E, spaces, etc.)
|
|
// \D matches any character that is NOT a digit (0-9)
|
|
let sanitized = value.replace(/\D/g, '');
|
|
|
|
// Remove leading zeros
|
|
// ^0+ matches one or more zeros at the start of the string
|
|
// This prevents issues like '007' remaining as '007' instead of '7'
|
|
// Note: A single '0' will become an empty string ''
|
|
sanitized = sanitized.replace(/^0+/, '');
|
|
|
|
return sanitized;
|
|
}
|
|
}
|