Files
ISA-Frontend/libs/utils/positive-integer-input/src/lib/positive-integer-input.directive.ts
Nino Righi cf359954ca Merged PR 2044: fix(utils-positive-integer-input): Fixed issue with copy and paste
fix(utils-positive-integer-input): Fixed issue with copy and paste

Ref: #5501
2025-11-21 15:40:55 +00:00

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