mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-47880 UI breaks when pasting very large text into text field (#12043)
This commit is contained in:
@@ -19,6 +19,7 @@ import { ContainerModel } from './container.model';
|
|||||||
import { ErrorMessageModel } from './error-message.model';
|
import { ErrorMessageModel } from './error-message.model';
|
||||||
import { FormFieldTypes } from './form-field-types';
|
import { FormFieldTypes } from './form-field-types';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_TEXT_MAX_LENGTH,
|
||||||
FixedValueFieldValidator,
|
FixedValueFieldValidator,
|
||||||
MaxLengthFieldValidator,
|
MaxLengthFieldValidator,
|
||||||
MaxValueFieldValidator,
|
MaxValueFieldValidator,
|
||||||
@@ -681,6 +682,42 @@ describe('FormFieldValidator', () => {
|
|||||||
expect(validator.isSupported(field)).toBe(true);
|
expect(validator.isSupported(field)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support text field when fallback maxLength is defined', () => {
|
||||||
|
const fallbackValidator = new MaxLengthFieldValidator([FormFieldTypes.TEXT], undefined, DEFAULT_TEXT_MAX_LENGTH);
|
||||||
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
|
type: FormFieldTypes.TEXT
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fallbackValidator.isSupported(field)).toBe(true);
|
||||||
|
expect(fallbackValidator.getMaxLength(field)).toBe(DEFAULT_TEXT_MAX_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate text field against fallback maxLength when maxLength is not configured', () => {
|
||||||
|
const fallbackValidator = new MaxLengthFieldValidator([FormFieldTypes.TEXT], undefined, DEFAULT_TEXT_MAX_LENGTH);
|
||||||
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
value: 'a'.repeat(DEFAULT_TEXT_MAX_LENGTH + 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
field.validationSummary = new ErrorMessageModel();
|
||||||
|
expect(fallbackValidator.validate(field)).toBe(false);
|
||||||
|
expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
expect(field.validationSummary.attributes.get('maxLength')).toBe(DEFAULT_TEXT_MAX_LENGTH.toLocaleString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use configured maxLength instead of fallback maxLength', () => {
|
||||||
|
const fallbackValidator = new MaxLengthFieldValidator([FormFieldTypes.TEXT], undefined, DEFAULT_TEXT_MAX_LENGTH);
|
||||||
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
maxLength: 3,
|
||||||
|
value: '1234'
|
||||||
|
});
|
||||||
|
|
||||||
|
field.validationSummary = new ErrorMessageModel();
|
||||||
|
expect(fallbackValidator.validate(field)).toBe(false);
|
||||||
|
expect(field.validationSummary.attributes.get('maxLength')).toBe('3');
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow empty values', () => {
|
it('should allow empty values', () => {
|
||||||
const field = new FormFieldModel(new FormModel(), {
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
type: FormFieldTypes.TEXT,
|
type: FormFieldTypes.TEXT,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { FormFieldTypes } from './form-field-types';
|
|||||||
import { isNumberValue } from './form-field-utils';
|
import { isNumberValue } from './form-field-utils';
|
||||||
import { FormFieldModel } from './form-field.model';
|
import { FormFieldModel } from './form-field.model';
|
||||||
|
|
||||||
|
export const DEFAULT_TEXT_MAX_LENGTH = 1024;
|
||||||
|
|
||||||
export interface FormFieldValidator {
|
export interface FormFieldValidator {
|
||||||
isSupported(field: FormFieldModel): boolean;
|
isSupported(field: FormFieldModel): boolean;
|
||||||
validate(field: FormFieldModel): boolean;
|
validate(field: FormFieldModel): boolean;
|
||||||
@@ -135,7 +137,8 @@ export class MinLengthFieldValidator implements FormFieldValidator {
|
|||||||
export class MaxLengthFieldValidator implements FormFieldValidator {
|
export class MaxLengthFieldValidator implements FormFieldValidator {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly supportedTypes: FormFieldTypes[] = [FormFieldTypes.TEXT, FormFieldTypes.MULTILINE_TEXT],
|
private readonly supportedTypes: FormFieldTypes[] = [FormFieldTypes.TEXT, FormFieldTypes.MULTILINE_TEXT],
|
||||||
private readonly maxLength?: number
|
private readonly maxLength?: number,
|
||||||
|
private readonly fallbackMaxLength?: number
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
isSupported(field: FormFieldModel): boolean {
|
isSupported(field: FormFieldModel): boolean {
|
||||||
@@ -158,7 +161,11 @@ export class MaxLengthFieldValidator implements FormFieldValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMaxLength(field: FormFieldModel): number | undefined {
|
getMaxLength(field: FormFieldModel): number | undefined {
|
||||||
return this.maxLength ?? field.maxLength;
|
if (this.maxLength !== undefined) {
|
||||||
|
return this.maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.maxLength > 0 ? field.maxLength : this.fallbackMaxLength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +324,8 @@ export const FORM_FIELD_VALIDATORS = [
|
|||||||
new RequiredFieldValidator(),
|
new RequiredFieldValidator(),
|
||||||
new NumberFieldValidator(),
|
new NumberFieldValidator(),
|
||||||
new MinLengthFieldValidator(),
|
new MinLengthFieldValidator(),
|
||||||
new MaxLengthFieldValidator(),
|
new MaxLengthFieldValidator([FormFieldTypes.TEXT], undefined, DEFAULT_TEXT_MAX_LENGTH),
|
||||||
|
new MaxLengthFieldValidator([FormFieldTypes.MULTILINE_TEXT]),
|
||||||
new MaxLengthFieldValidator([FormFieldTypes.NUMBER], 10),
|
new MaxLengthFieldValidator([FormFieldTypes.NUMBER], 10),
|
||||||
new MinValueFieldValidator(),
|
new MinValueFieldValidator(),
|
||||||
new MaxValueFieldValidator(),
|
new MaxValueFieldValidator(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="adf-textfield adf-text-widget {{ field.className }}"
|
<div class="adf-textfield adf-text-widget {{ field.className }}"
|
||||||
[class.adf-invalid]="!field.isValid && isTouched()"
|
[class.adf-invalid]="(!field.isValid || maxLengthPasteError.isActive()) && isTouched()"
|
||||||
[class.adf-readonly]="field.readOnly"
|
[class.adf-readonly]="field.readOnly"
|
||||||
[class.adf-left-label-input-container]="field.leftLabels">
|
[class.adf-left-label-input-container]="field.leftLabels">
|
||||||
<div *ngIf="field.leftLabels">
|
<div *ngIf="field.leftLabels">
|
||||||
@@ -26,15 +26,32 @@
|
|||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
[title]="field.tooltip"
|
[title]="field.tooltip"
|
||||||
[errorStateMatcher]="errorStateMatcher"
|
[errorStateMatcher]="errorStateMatcher"
|
||||||
|
(input)="onInput($event)"
|
||||||
|
(paste)="onPaste($event)"
|
||||||
(blur)="onBlur()">
|
(blur)="onBlur()">
|
||||||
@if (!fieldStatusTemplate && (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched()))) {
|
@if (!fieldStatusTemplate && (maxLengthPasteError.isActive() || field.validationSummary?.message || (isInvalidFieldRequired() && isTouched()))) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||||
<span class="adf-error-text"
|
<span class="adf-error-text">
|
||||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
@if (maxLengthPasteError.isActive()) {
|
||||||
|
{{ maxLengthPasteError.message | translate:maxLengthPasteErrorParameters }}
|
||||||
|
} @else if (field.validationSummary?.message) {
|
||||||
|
{{ field.validationSummary.message | translate:translateParameters }}
|
||||||
|
} @else {
|
||||||
|
{{ 'FORM.FIELD.REQUIRED' | translate }}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</mat-error>
|
</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<ng-container *ngTemplateOutlet="fieldStatusTemplate ?? null; context: { $implicit: this }" />
|
<ng-container *ngTemplateOutlet="maxLengthPasteError.isActive() && fieldStatusTemplate ? maxLengthPasteErrorTemplate : (fieldStatusTemplate ?? null); context: { $implicit: this }" />
|
||||||
|
<ng-template #maxLengthPasteErrorTemplate>
|
||||||
|
<div class="adf-error-container adf-error-messages-container">
|
||||||
|
<div class="adf-error">
|
||||||
|
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||||
|
<div class="adf-error-text">{{ maxLengthPasteError.message | translate:maxLengthPasteErrorParameters }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,6 +142,186 @@ describe('TextWidgetComponent', () => {
|
|||||||
expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should validate against default max length when maxLength is not configured', async () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
await testingUtils.fillMatInput('a'.repeat(1025));
|
||||||
|
|
||||||
|
expect(widget.field.isValid).toBe(false);
|
||||||
|
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
expect(widget.field.validationSummary.attributes.get('maxLength')).toBe('1,024');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block paste when resulting value exceeds configured max length', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '1234',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '1234';
|
||||||
|
inputElement.setSelectionRange(4, 4);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => '56' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
|
||||||
|
expect(pasteEvent.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(widget.maxLengthPasteError.message).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
expect(widget.maxLengthPasteError.attributes.get('maxLength')).toBe('5');
|
||||||
|
|
||||||
|
widget.field.validate();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const errorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
|
||||||
|
expect(errorWidget.textContent.trim()).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
expect(widget.maxLengthPasteError.isActive()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep paste max length error when value change emits after blocked paste', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '1234',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '1234';
|
||||||
|
inputElement.setSelectionRange(4, 4);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => '56' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
widget.onValueChange('123456');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(widget.maxLengthPasteError.isActive()).toBe(true);
|
||||||
|
const errorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
|
||||||
|
expect(errorWidget.textContent.trim()).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep paste max length error when paste input event emits after blocked paste', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '1234',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '1234';
|
||||||
|
inputElement.setSelectionRange(4, 4);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => '56' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
|
||||||
|
widget.onInput({ inputType: 'insertFromPaste' } as unknown as Event);
|
||||||
|
|
||||||
|
expect(widget.maxLengthPasteError.isActive()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear paste max length error when non-paste input happens after blocked paste', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '1234',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '1234';
|
||||||
|
inputElement.setSelectionRange(4, 4);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => '56' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
widget.onInput({ inputType: 'insertText' } as unknown as Event);
|
||||||
|
|
||||||
|
expect(widget.maxLengthPasteError.isActive()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow paste when selected text keeps resulting value within max length', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '12345',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '12345';
|
||||||
|
inputElement.setSelectionRange(2, 5);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => 'abc' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
|
||||||
|
expect(pasteEvent.preventDefault).not.toHaveBeenCalled();
|
||||||
|
expect(widget.maxLengthPasteError.isActive()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block paste when resulting value exceeds default max length', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: 'a'.repeat(1024),
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = 'a'.repeat(1024);
|
||||||
|
inputElement.setSelectionRange(1024, 1024);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => 'b' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
|
||||||
|
expect(pasteEvent.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(widget.maxLengthPasteError.message).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
expect(widget.maxLengthPasteError.attributes.get('maxLength')).toBe('1,024');
|
||||||
|
expect(widget.maxLengthPasteError.isActive()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to set regex pattern property for Text widget', async () => {
|
it('should be able to set regex pattern property for Text widget', async () => {
|
||||||
widget.field = new FormFieldModel(form, {
|
widget.field = new FormFieldModel(form, {
|
||||||
id: 'text-id',
|
id: 'text-id',
|
||||||
@@ -505,6 +685,59 @@ describe('TextWidgetComponent', () => {
|
|||||||
const customStatusMessage = testingUtils.getByCSS('.custom-status-message').nativeElement;
|
const customStatusMessage = testingUtils.getByCSS('.custom-status-message').nativeElement;
|
||||||
expect(customStatusMessage?.textContent).toBe(`custom status message for ${widget.field.name}`);
|
expect(customStatusMessage?.textContent).toBe(`custom status message for ${widget.field.name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should display paste max length error with custom status template', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '1234',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '1234';
|
||||||
|
inputElement.setSelectionRange(4, 4);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => '56' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const pasteErrorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
|
||||||
|
expect(pasteErrorWidget.innerHTML).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace custom status template with paste max length error', () => {
|
||||||
|
widget.field = new FormFieldModel(form, {
|
||||||
|
id: 'text-id',
|
||||||
|
name: 'text-name',
|
||||||
|
value: '1234',
|
||||||
|
type: FormFieldTypes.TEXT,
|
||||||
|
readOnly: false,
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const inputElement = testingUtils.getByCSS('#text-id').nativeElement;
|
||||||
|
inputElement.value = '1234';
|
||||||
|
inputElement.setSelectionRange(4, 4);
|
||||||
|
const pasteEvent = {
|
||||||
|
target: inputElement,
|
||||||
|
clipboardData: { getData: () => '56' },
|
||||||
|
preventDefault: jasmine.createSpy('preventDefault')
|
||||||
|
} as unknown as ClipboardEvent;
|
||||||
|
|
||||||
|
widget.onPaste(pasteEvent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(testingUtils.getByCSS('.custom-status-message')).toBeNull();
|
||||||
|
const pasteErrorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
|
||||||
|
expect(pasteErrorWidget.innerHTML).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { WidgetComponent } from '../widget.component';
|
import { WidgetComponent } from '../widget.component';
|
||||||
|
import { ErrorMessageModel } from '../core/error-message.model';
|
||||||
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
|
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
|
||||||
|
import { DEFAULT_TEXT_MAX_LENGTH } from '../core/form-field-validator';
|
||||||
import { InputMaskDirective } from './text-mask.component';
|
import { InputMaskDirective } from './text-mask.component';
|
||||||
import { IconModule } from '../../../../icon/icon.module';
|
import { IconModule } from '../../../../icon/icon.module';
|
||||||
|
|
||||||
@@ -67,12 +69,21 @@ export class FieldStatusTemplateDirective {
|
|||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||||
mask: string;
|
mask = '';
|
||||||
placeholder: string;
|
placeholder = '';
|
||||||
isMaskReversed: boolean;
|
isMaskReversed = false;
|
||||||
fieldStatusTemplate = inject(FIELD_STATUS_TEMPLATE, { optional: true });
|
fieldStatusTemplate = inject(FIELD_STATUS_TEMPLATE, { optional: true });
|
||||||
errorStateMatcher: ErrorStateMatcher;
|
errorStateMatcher!: ErrorStateMatcher;
|
||||||
translateParameters: Record<string, string> = {};
|
translateParameters: Record<string, string> = {};
|
||||||
|
maxLengthPasteError = new ErrorMessageModel();
|
||||||
|
|
||||||
|
get resolvedMaxLength(): number {
|
||||||
|
return this.field?.maxLength > 0 ? this.field.maxLength : DEFAULT_TEXT_MAX_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxLengthPasteErrorParameters(): Record<string, string> {
|
||||||
|
return this.maxLengthPasteError.getAttributesAsJsonObj();
|
||||||
|
}
|
||||||
|
|
||||||
override ngOnInit() {
|
override ngOnInit() {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
@@ -88,19 +99,28 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
|||||||
this.initErrorStateMatcher();
|
this.initErrorStateMatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initErrorStateMatcher(): void {
|
onPaste(event: ClipboardEvent): void {
|
||||||
this.errorStateMatcher = {
|
const input = event.target instanceof HTMLInputElement ? event.target : null;
|
||||||
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
|
const pastedValue = event.clipboardData?.getData('text') ?? '';
|
||||||
!this.fieldStatusTemplate && (!!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched()))
|
|
||||||
};
|
if (!input || this.getLengthAfterPaste(input, pastedValue) <= this.resolvedMaxLength) {
|
||||||
|
this.clearMaxLengthPasteError();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTranslateParameters(): void {
|
event.preventDefault();
|
||||||
if (this.field.validationSummary?.isActive()) {
|
this.markAsTouched();
|
||||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
this.setMaxLengthPasteError();
|
||||||
} else {
|
|
||||||
this.translateParameters = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInput(event: Event): void {
|
||||||
|
const inputType = 'inputType' in event ? event.inputType : undefined;
|
||||||
|
|
||||||
|
if (inputType === 'insertFromPaste') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearMaxLengthPasteError();
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlur(): void {
|
onBlur(): void {
|
||||||
@@ -112,4 +132,41 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
|||||||
this.onFieldChanged(this.field);
|
this.onFieldChanged(this.field);
|
||||||
this.updateTranslateParameters();
|
this.updateTranslateParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initErrorStateMatcher(): void {
|
||||||
|
this.errorStateMatcher = {
|
||||||
|
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
|
||||||
|
!this.fieldStatusTemplate &&
|
||||||
|
(this.maxLengthPasteError.isActive() ||
|
||||||
|
!!this.field.validationSummary?.message ||
|
||||||
|
(this.isInvalidFieldRequired() && this.isTouched()))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTranslateParameters(): void {
|
||||||
|
if (this.field.validationSummary?.isActive()) {
|
||||||
|
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||||
|
} else {
|
||||||
|
this.translateParameters = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLengthAfterPaste(input: HTMLInputElement, pastedValue: string): number {
|
||||||
|
const value = input.value ?? '';
|
||||||
|
const selectionStart = input.selectionStart ?? value.length;
|
||||||
|
const selectionEnd = input.selectionEnd ?? selectionStart;
|
||||||
|
|
||||||
|
return value.length - Math.max(selectionEnd - selectionStart, 0) + pastedValue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setMaxLengthPasteError(): void {
|
||||||
|
this.maxLengthPasteError = new ErrorMessageModel({
|
||||||
|
message: 'FORM.FIELD.VALIDATOR.NO_LONGER_THAN',
|
||||||
|
attributes: new Map([['maxLength', this.resolvedMaxLength.toLocaleString()]])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearMaxLengthPasteError(): void {
|
||||||
|
this.maxLengthPasteError = new ErrorMessageModel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ import {
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
FormFieldEvent,
|
FormFieldEvent,
|
||||||
NoopTranslateModule,
|
NoopTranslateModule,
|
||||||
NoopAuthModule
|
NoopAuthModule,
|
||||||
|
FORM_FIELD_VALIDATORS
|
||||||
} from '@alfresco/adf-core';
|
} from '@alfresco/adf-core';
|
||||||
import { Node } from '@alfresco/js-api';
|
import { Node } from '@alfresco/js-api';
|
||||||
import { ESCAPE } from '@angular/cdk/keycodes';
|
import { ESCAPE } from '@angular/cdk/keycodes';
|
||||||
@@ -1394,7 +1395,7 @@ describe('FormCloudComponent', () => {
|
|||||||
formComponent.formCloudRepresentationJSON = new FormCloudRepresentation(JSON.parse(JSON.stringify(cloudFormMock)));
|
formComponent.formCloudRepresentationJSON = new FormCloudRepresentation(JSON.parse(JSON.stringify(cloudFormMock)));
|
||||||
const form = formComponent.parseForm(formComponent.formCloudRepresentationJSON);
|
const form = formComponent.parseForm(formComponent.formCloudRepresentationJSON);
|
||||||
expect(formComponent.fieldValidators.length).toBe(1);
|
expect(formComponent.fieldValidators.length).toBe(1);
|
||||||
expect(form.fieldValidators.length).toBe(11);
|
expect(form.fieldValidators.length).toBe(FORM_FIELD_VALIDATORS.length + formComponent.fieldValidators.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('form validations', () => {
|
describe('form validations', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user