UNPKG

@clr/angular

Version:

Angular components for Clarity

827 lines (807 loc) 40.2 kB
import * as i0 from '@angular/core'; import { InjectionToken, Component, inject, TemplateRef, Directive, Injector, ContentChild, ViewChild, forwardRef, Input, HostListener, Self, Optional, NgModule } from '@angular/core'; import * as i3 from '@clr/angular/forms/common'; import { NgControlService, ClrAbstractContainer, ControlIdService, ControlClassService, WrappedFormControl, ClrCommonFormsModule } from '@clr/angular/forms/common'; import * as i2 from '@clr/angular/utils'; import { ClrCommonStringsService } from '@clr/angular/utils'; import * as i1 from '@clr/angular/icon'; import { ClarityIcons, folderOpenIcon, successStandardIcon, errorStandardIcon, ClrIcon } from '@clr/angular/icon'; import * as i1$1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i1$2 from '@angular/forms'; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms'; /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ function buildFileList(files) { const dataTransfer = new DataTransfer(); for (const file of files) { dataTransfer.items.add(file); } return dataTransfer.files; } function selectFiles(fileInputElement, files) { fileInputElement.files = files instanceof FileList ? files : buildFileList(files); fileInputElement.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } function clearFiles(fileInputElement) { fileInputElement.value = ''; fileInputElement.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ const CLR_FILE_MESSAGES_TEMPLATE_CONTEXT = new InjectionToken('ClrFileMessagesTemplateContext'); class ClrFileInfo { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInfo, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: ClrFileInfo, isStandalone: false, selector: "clr-file-info", host: { properties: { "class.clr-subtext-wrapper": "true" } }, ngImport: i0, template: ` <span class="clr-subtext"> <ng-content></ng-content> </span> `, isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInfo, decorators: [{ type: Component, args: [{ selector: 'clr-file-info', template: ` <span class="clr-subtext"> <ng-content></ng-content> </span> `, host: { '[class.clr-subtext-wrapper]': 'true', }, standalone: false, }] }] }); class ClrFileSuccess { constructor() { this.context = inject(CLR_FILE_MESSAGES_TEMPLATE_CONTEXT); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileSuccess, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrFileSuccess, isStandalone: false, selector: "clr-file-success", host: { properties: { "style.display": "context.success ? \"flex\" : \"none\"", "class.clr-subtext-wrapper": "true", "class.success": "true" } }, ngImport: i0, template: ` @if (context.success) { <cds-icon class="clr-validate-icon" shape="success-standard" status="success" aria-hidden="true"></cds-icon> <span class="clr-subtext"> <ng-content></ng-content> </span> } `, isInline: true, dependencies: [{ kind: "component", type: i1.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileSuccess, decorators: [{ type: Component, args: [{ selector: 'clr-file-success', // We check for success here so that consumers don't have to. template: ` @if (context.success) { <cds-icon class="clr-validate-icon" shape="success-standard" status="success" aria-hidden="true"></cds-icon> <span class="clr-subtext"> <ng-content></ng-content> </span> } `, host: { '[style.display]': 'context.success ? "flex" : "none"', '[class.clr-subtext-wrapper]': 'true', '[class.success]': 'true', }, standalone: false, }] }] }); class ClrFileError { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileError, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: ClrFileError, isStandalone: false, selector: "clr-file-error", host: { properties: { "class.clr-subtext-wrapper": "true", "class.error": "true" } }, ngImport: i0, template: ` <cds-icon class="clr-validate-icon" shape="error-standard" status="danger" aria-hidden="true"></cds-icon> <span class="clr-subtext"> <ng-content></ng-content> </span> `, isInline: true, dependencies: [{ kind: "component", type: i1.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileError, decorators: [{ type: Component, args: [{ selector: 'clr-file-error', // The host should have an `*ngIf` or `@if` that checks for the relevant error. template: ` <cds-icon class="clr-validate-icon" shape="error-standard" status="danger" aria-hidden="true"></cds-icon> <span class="clr-subtext"> <ng-content></ng-content> </span> `, host: { '[class.clr-subtext-wrapper]': 'true', '[class.error]': 'true', }, standalone: false, }] }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileMessagesTemplate { constructor() { this.templateRef = inject(TemplateRef); } static ngTemplateContextGuard(directive, context) { return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileMessagesTemplate, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrFileMessagesTemplate, isStandalone: false, selector: "ng-template[clr-file-messages]", ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileMessagesTemplate, decorators: [{ type: Directive, args: [{ selector: 'ng-template[clr-file-messages]', standalone: false, }] }] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileList { constructor() { this.injectorCache = new Map(); this.contextCache = new Map(); this.injector = inject(Injector); this.commonStrings = inject(ClrCommonStringsService); this.ngControlService = inject(NgControlService, { optional: true }); this.fileInputContainer = inject(ClrFileInputContainer, { optional: true }); if (!this.ngControlService || !this.fileInputContainer) { throw new Error('The clr-file-list component can only be used within a clr-file-input-container.'); } } get files() { if (!this.fileInputContainer.fileInput) { return []; } const fileInputElement = this.fileInputContainer.fileInput.elementRef.nativeElement; return Array.from(fileInputElement.files).sort((a, b) => a.name.localeCompare(b.name)); } getClearFileLabel(filename) { return this.commonStrings.parse(this.commonStrings.keys.clearFile, { FILE: filename, }); } clearFile(fileToRemove) { if (!this.fileInputContainer.fileInput) { return; } const fileInputElement = this.fileInputContainer.fileInput.elementRef.nativeElement; const files = Array.from(fileInputElement.files); const newFiles = files.filter(file => file !== fileToRemove); selectFiles(fileInputElement, newFiles); this.fileInputContainer.focusBrowseButton(); } createFileMessagesTemplateContext(file) { const fileInputErrors = this.ngControlService.controls[0].errors || {}; const errors = { accept: fileInputErrors.accept?.find(error => error.name === file.name), minFileSize: fileInputErrors.minFileSize?.find(error => error.name === file.name), maxFileSize: fileInputErrors.maxFileSize?.find(error => error.name === file.name), }; const success = Object.values(errors).every(error => !error); const cached = this.contextCache.get(file); if (cached && cached.success === success && this.errorsEqual(cached.errors, errors)) { return cached; } // new context is made and old reference replaced const context = { $implicit: file, success, errors }; this.contextCache.set(file, context); // new injector is made and old reference replaced const injector = this.createFileMessagesTemplateInjector(context); this.injectorCache.set(file, injector); return context; } createFileMessagesTemplateInjector(fileMessagesTemplateContext) { return Injector.create({ parent: this.injector, providers: [{ provide: CLR_FILE_MESSAGES_TEMPLATE_CONTEXT, useValue: fileMessagesTemplateContext }], }); } errorsEqual(a, b) { return a.accept === b.accept && a.minFileSize === b.minFileSize && a.maxFileSize === b.maxFileSize; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileList, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrFileList, isStandalone: false, selector: "clr-file-list", host: { properties: { "attr.role": "\"list\"", "class.clr-file-list": "true" } }, queries: [{ propertyName: "fileMessagesTemplate", first: true, predicate: ClrFileMessagesTemplate, descendants: true }], ngImport: i0, template: ` @for (file of files; track file) { @if (createFileMessagesTemplateContext(file); as fileMessagesTemplateContext) { <div role="listitem" class="clr-file-list-item" [ngClass]="{ 'clr-error': !fileMessagesTemplateContext.success, 'clr-success': fileMessagesTemplateContext.success, }" > <div class="clr-file-label-and-status-icon"> <span class="label clr-file-label"> {{ file.name }} <button class="btn btn-sm btn-link-neutral btn-icon clr-file-clear-button" [attr.aria-label]="getClearFileLabel(file.name)" (click)="clearFile(file)" > <cds-icon shape="times"></cds-icon> </button> </span> </div> @if (fileMessagesTemplate) { <ng-container [ngTemplateOutlet]="fileMessagesTemplate.templateRef" [ngTemplateOutletContext]="fileMessagesTemplateContext" [ngTemplateOutletInjector]="injectorCache.get(file)" ></ng-container> } </div> } } `, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i1.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileList, decorators: [{ type: Component, args: [{ selector: 'clr-file-list', template: ` @for (file of files; track file) { @if (createFileMessagesTemplateContext(file); as fileMessagesTemplateContext) { <div role="listitem" class="clr-file-list-item" [ngClass]="{ 'clr-error': !fileMessagesTemplateContext.success, 'clr-success': fileMessagesTemplateContext.success, }" > <div class="clr-file-label-and-status-icon"> <span class="label clr-file-label"> {{ file.name }} <button class="btn btn-sm btn-link-neutral btn-icon clr-file-clear-button" [attr.aria-label]="getClearFileLabel(file.name)" (click)="clearFile(file)" > <cds-icon shape="times"></cds-icon> </button> </span> </div> @if (fileMessagesTemplate) { <ng-container [ngTemplateOutlet]="fileMessagesTemplate.templateRef" [ngTemplateOutletContext]="fileMessagesTemplateContext" [ngTemplateOutletInjector]="injectorCache.get(file)" ></ng-container> } </div> } } `, host: { '[attr.role]': '"list"', '[class.clr-file-list]': 'true', }, standalone: false, }] }], ctorParameters: () => [], propDecorators: { fileMessagesTemplate: [{ type: ContentChild, args: [ClrFileMessagesTemplate] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileInputContainer extends ClrAbstractContainer { constructor() { super(...arguments); this.commonStrings = inject(ClrCommonStringsService); } get accept() { return this.fileInput.elementRef.nativeElement.accept; } get multiple() { return this.fileInput.elementRef.nativeElement.multiple; } get disabled() { return this.control ? this.control.disabled : this.fileInput.elementRef.nativeElement.disabled; } get browseButtonText() { const selectionButtonLabel = this.fileList ? undefined : this.fileInput?.selection?.buttonLabel; return selectionButtonLabel || this.customButtonLabel || this.commonStrings.keys.browse; } get browseButtonDescribedBy() { return `${this.label?.forAttr} ${this.fileInput.elementRef.nativeElement.getAttribute('aria-describedby')}`; } get successMessagePresent() { return super.successMessagePresent || !!this.fileSuccessComponent; } get errorMessagePresent() { return super.errorMessagePresent || !!this.fileErrorComponent; } focusBrowseButton() { this.browseButtonElementRef.nativeElement.focus(); } browse() { const fileInputElementRef = this.fileList && this.multiple ? this.fileListFileInputElementRef : this.fileInput.elementRef; fileInputElementRef.nativeElement.click(); } clearSelectedFiles() { this.fileInput.elementRef.nativeElement.value = ''; this.fileInput.elementRef.nativeElement.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); this.focusBrowseButton(); } addFilesToSelection(newFiles) { if (!newFiles.length) { return; } // start with new files const mergedFiles = [...newFiles]; // add existing files if a new file doesn't have the same name for (const existingFile of this.fileInput.elementRef.nativeElement.files) { if (!mergedFiles.some(file => file.name === existingFile.name)) { mergedFiles.push(existingFile); } } // update file selection selectFiles(this.fileInput.elementRef.nativeElement, mergedFiles); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputContainer, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ClrFileInputContainer, isStandalone: false, selector: "clr-file-input-container", inputs: { customButtonLabel: ["clrButtonLabel", "customButtonLabel"] }, host: { properties: { "class.clr-form-control": "true", "class.clr-form-control-disabled": "disabled", "class.clr-row": "addGrid()" } }, providers: [NgControlService, ControlIdService, ControlClassService], queries: [{ propertyName: "fileInput", first: true, predicate: i0.forwardRef(() => ClrFileInput), descendants: true }, { propertyName: "fileList", first: true, predicate: i0.forwardRef(() => ClrFileList), descendants: true }, { propertyName: "fileSuccessComponent", first: true, predicate: ClrFileSuccess, descendants: true }, { propertyName: "fileErrorComponent", first: true, predicate: ClrFileError, descendants: true }], viewQueries: [{ propertyName: "browseButtonElementRef", first: true, predicate: ["browseButton"], descendants: true }, { propertyName: "fileListFileInputElementRef", first: true, predicate: ["fileListFileInput"], descendants: true }], usesInheritance: true, ngImport: i0, template: ` <ng-content select="label"></ng-content> @if (!label && addGrid()) { <label></label> } <div class="clr-control-container" [ngClass]="controlClass()"> <div class="clr-file-input-wrapper"> <ng-content select="[clrFileInput]"></ng-content> <!-- file input to handle adding new files to selection when file list is present (prevent replacing selected files on the main file input) --> @if (fileList) { <input #fileListFileInput type="file" class="clr-file-input" tabindex="-1" aria-hidden="true" [accept]="accept" [multiple]="multiple" [disabled]="disabled" (change)="addFilesToSelection(fileListFileInput.files)" /> } <button #browseButton type="button" class="btn btn-sm clr-file-input-browse-button" [attr.aria-describedby]="browseButtonDescribedBy" [disabled]="disabled" (click)="browse()" > <cds-icon shape="folder-open"></cds-icon> <span class="clr-file-input-browse-button-text">{{ browseButtonText }}</span> </button> @if (!fileList && fileInput?.selection?.fileCount) { <button type="button" class="btn btn-sm clr-file-input-clear-button" [attr.aria-label]="fileInput?.selection?.clearFilesButtonLabel" (click)="clearSelectedFiles()" > <cds-icon shape="times" status="neutral"></cds-icon> </button> } </div> @if (showHelper) { <ng-content select="clr-control-helper"></ng-content> } @if (showInvalid) { <ng-content select="clr-control-error"></ng-content> } @if (showValid) { <ng-content select="clr-control-success"></ng-content> } <!-- If this is present, this file input becomes an "advanced" file input. --> <ng-container> <div class="clr-file-list-break"></div> <ng-content select="clr-file-list"></ng-content> </ng-container> </div> `, isInline: true, dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: i1.ClrIcon, selector: "clr-icon, cds-icon", inputs: ["shape", "size", "direction", "flip", "solid", "status", "inverse", "badge"] }, { kind: "directive", type: i3.ClrControlLabel, selector: "label", inputs: ["id", "for"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputContainer, decorators: [{ type: Component, args: [{ selector: 'clr-file-input-container', template: ` <ng-content select="label"></ng-content> @if (!label && addGrid()) { <label></label> } <div class="clr-control-container" [ngClass]="controlClass()"> <div class="clr-file-input-wrapper"> <ng-content select="[clrFileInput]"></ng-content> <!-- file input to handle adding new files to selection when file list is present (prevent replacing selected files on the main file input) --> @if (fileList) { <input #fileListFileInput type="file" class="clr-file-input" tabindex="-1" aria-hidden="true" [accept]="accept" [multiple]="multiple" [disabled]="disabled" (change)="addFilesToSelection(fileListFileInput.files)" /> } <button #browseButton type="button" class="btn btn-sm clr-file-input-browse-button" [attr.aria-describedby]="browseButtonDescribedBy" [disabled]="disabled" (click)="browse()" > <cds-icon shape="folder-open"></cds-icon> <span class="clr-file-input-browse-button-text">{{ browseButtonText }}</span> </button> @if (!fileList && fileInput?.selection?.fileCount) { <button type="button" class="btn btn-sm clr-file-input-clear-button" [attr.aria-label]="fileInput?.selection?.clearFilesButtonLabel" (click)="clearSelectedFiles()" > <cds-icon shape="times" status="neutral"></cds-icon> </button> } </div> @if (showHelper) { <ng-content select="clr-control-helper"></ng-content> } @if (showInvalid) { <ng-content select="clr-control-error"></ng-content> } @if (showValid) { <ng-content select="clr-control-success"></ng-content> } <!-- If this is present, this file input becomes an "advanced" file input. --> <ng-container> <div class="clr-file-list-break"></div> <ng-content select="clr-file-list"></ng-content> </ng-container> </div> `, host: { '[class.clr-form-control]': 'true', '[class.clr-form-control-disabled]': 'disabled', '[class.clr-row]': 'addGrid()', }, providers: [NgControlService, ControlIdService, ControlClassService], standalone: false, }] }], propDecorators: { customButtonLabel: [{ type: Input, args: ['clrButtonLabel'] }], fileInput: [{ type: ContentChild, args: [forwardRef(() => ClrFileInput)] }], fileList: [{ type: ContentChild, args: [forwardRef(() => ClrFileList)] }], browseButtonElementRef: [{ type: ViewChild, args: ['browseButton'] }], fileListFileInputElementRef: [{ type: ViewChild, args: ['fileListFileInput'] }], fileSuccessComponent: [{ type: ContentChild, args: [ClrFileSuccess] }], fileErrorComponent: [{ type: ContentChild, args: [ClrFileError] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileInput extends WrappedFormControl { constructor(injector, renderer, viewContainerRef, elementRef, control, commonStrings) { super(viewContainerRef, ClrFileInputContainer, injector, control, renderer, elementRef); this.elementRef = elementRef; this.control = control; this.commonStrings = commonStrings; this.selection = undefined; } handleChange() { this.updateSelection(); } updateSelection() { const files = this.elementRef.nativeElement.files; let selectionButtonLabel; let clearFilesButtonLabel; if (files?.length === 1) { const filename = files[0].name; selectionButtonLabel = filename; clearFilesButtonLabel = this.commonStrings.parse(this.commonStrings.keys.clearFile, { FILE: filename, }); } else if (files?.length > 1) { const fileCount = files.length.toString(); selectionButtonLabel = this.commonStrings.parse(this.commonStrings.keys.fileCount, { COUNT: fileCount, }); clearFilesButtonLabel = this.commonStrings.parse(this.commonStrings.keys.clearFiles, { COUNT: fileCount, }); } this.selection = { fileCount: files.length, buttonLabel: selectionButtonLabel, clearFilesButtonLabel, }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInput, deps: [{ token: i0.Injector }, { token: i0.Renderer2 }, { token: i0.ViewContainerRef }, { token: i0.ElementRef }, { token: i1$2.NgControl, optional: true, self: true }, { token: i2.ClrCommonStringsService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrFileInput, isStandalone: false, selector: "input[type=\"file\"][clrFileInput]", host: { attributes: { "tabindex": "-1", "aria-hidden": "true" }, listeners: { "change": "handleChange()" }, properties: { "class.clr-file-input": "true" } }, usesInheritance: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInput, decorators: [{ type: Directive, args: [{ selector: 'input[type="file"][clrFileInput]', host: { tabindex: '-1', // Remove the hidden file `input` element from the tab order because the browse `button` replaces it. 'aria-hidden': 'true', // Remove the hidden file `input` element from the accessibility tree because the browse `button` replaces it. '[class.clr-file-input]': 'true', }, standalone: false, }] }], ctorParameters: () => [{ type: i0.Injector }, { type: i0.Renderer2 }, { type: i0.ViewContainerRef }, { type: i0.ElementRef }, { type: i1$2.NgControl, decorators: [{ type: Self }, { type: Optional }] }, { type: i2.ClrCommonStringsService }], propDecorators: { handleChange: [{ type: HostListener, args: ['change'] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileInputValidator { constructor(elementRef) { this.elementRef = elementRef; } validate(control) { const files = control.value; const fileInputElement = this.elementRef.nativeElement; const errors = {}; // required validation (native attribute) if (fileInputElement.required && files?.length === 0) { errors.required = true; } const accept = fileInputElement.accept ? fileInputElement.accept.split(',').map(type => type.trim()) : null; if (files?.length > 0 && (accept || this.minFileSize || this.maxFileSize)) { for (let i = 0; i < files.length; i++) { const file = files.item(i); // accept validation (native attribute) if (accept && accept.length) { if (!this.validateAccept(file, accept)) { errors.accept = errors.accept || []; errors.accept.push({ name: file.name, accept, type: file.type || '', extension: this.getSuffixByDepth(file.name, 2), // last up to 2 parts for reporting }); } } // min file validation (custom input) if (this.minFileSize && file.size < this.minFileSize) { errors.minFileSize = errors.minFileSize || []; errors.minFileSize.push({ name: file.name, minFileSize: this.minFileSize, actualFileSize: file.size }); } // max file validation (custom input) if (this.maxFileSize && file.size > this.maxFileSize) { errors.maxFileSize = errors.maxFileSize || []; errors.maxFileSize.push({ name: file.name, maxFileSize: this.maxFileSize, actualFileSize: file.size }); } } } return Object.keys(errors).length ? errors : null; } getSuffixByDepth(filename, depth) { const match = filename.toLowerCase().match(new RegExp(`(\\.[^.]+){1,${depth}}$`, 'i')); return match ? match[0] : ''; } validateAccept(file, acceptList) { const name = file.name.toLowerCase(); const type = (file.type || '').toLowerCase(); for (const entryRaw of acceptList) { const entry = entryRaw.trim().toLowerCase(); if (!entry) { continue; } // Extension check if (entry.startsWith('.')) { const depth = (entry.match(/\./g) || []).length; if (this.getSuffixByDepth(name, depth) === entry) { return true; } continue; } // MIME check if (entry.endsWith('/*')) { const prefix = entry.slice(0, entry.length - 1); // keep trailing slash if (type.startsWith(prefix)) { return true; } } else if (entry.includes('/') && type === entry) { return true; } } return false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputValidator, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrFileInputValidator, isStandalone: false, selector: "input[type=\"file\"][clrFileInput]", inputs: { minFileSize: ["clrMinFileSize", "minFileSize"], maxFileSize: ["clrMaxFileSize", "maxFileSize"] }, providers: [{ provide: NG_VALIDATORS, useExisting: ClrFileInputValidator, multi: true }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputValidator, decorators: [{ type: Directive, args: [{ selector: 'input[type="file"][clrFileInput]', providers: [{ provide: NG_VALIDATORS, useExisting: ClrFileInputValidator, multi: true }], standalone: false, }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { minFileSize: [{ type: Input, args: ['clrMinFileSize'] }], maxFileSize: [{ type: Input, args: ['clrMaxFileSize'] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileInputValueAccessor { constructor(elementRef) { this.elementRef = elementRef; this.onChange = (_value) => { }; this.onTouched = () => { }; } writeValue(value) { if (value !== undefined && value !== null && !(value instanceof FileList)) { throw new Error('The value of a file input control must be a FileList.'); } if (value) { selectFiles(this.elementRef.nativeElement, value); } else if (this.elementRef.nativeElement.files.length) { clearFiles(this.elementRef.nativeElement); } } registerOnChange(fn) { this.onChange = fn; } registerOnTouched(fn) { this.onTouched = fn; } setDisabledState(isDisabled) { this.elementRef.nativeElement.disabled = isDisabled; } handleChange() { this.onTouched(); this.onChange(this.elementRef.nativeElement.files); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputValueAccessor, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrFileInputValueAccessor, isStandalone: false, selector: "input[type=\"file\"][clrFileInput]", host: { listeners: { "change": "handleChange()" } }, providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: ClrFileInputValueAccessor, multi: true }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputValueAccessor, decorators: [{ type: Directive, args: [{ selector: 'input[type="file"][clrFileInput]', providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: ClrFileInputValueAccessor, multi: true }], standalone: false, }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { handleChange: [{ type: HostListener, args: ['change'] }] } }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ class ClrFileInputModule { constructor() { ClarityIcons.addIcons(folderOpenIcon, successStandardIcon, errorStandardIcon); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputModule, declarations: [ClrFileInput, ClrFileInputContainer, ClrFileInputValidator, ClrFileInputValueAccessor, ClrFileList, ClrFileMessagesTemplate, ClrFileInfo, ClrFileSuccess, ClrFileError], imports: [CommonModule, ClrIcon, ClrCommonFormsModule], exports: [ClrCommonFormsModule, ClrFileInput, ClrFileInputContainer, ClrFileInputValidator, ClrFileInputValueAccessor, ClrFileList, ClrFileMessagesTemplate, ClrFileInfo, ClrFileSuccess, ClrFileError] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputModule, imports: [CommonModule, ClrIcon, ClrCommonFormsModule, ClrCommonFormsModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrFileInputModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule, ClrIcon, ClrCommonFormsModule], declarations: [ ClrFileInput, ClrFileInputContainer, ClrFileInputValidator, ClrFileInputValueAccessor, ClrFileList, ClrFileMessagesTemplate, ClrFileInfo, ClrFileSuccess, ClrFileError, ], exports: [ ClrCommonFormsModule, ClrFileInput, ClrFileInputContainer, ClrFileInputValidator, ClrFileInputValueAccessor, ClrFileList, ClrFileMessagesTemplate, ClrFileInfo, ClrFileSuccess, ClrFileError, ], }] }], ctorParameters: () => [] }); /* * Copyright (c) 2016-2026 Broadcom. All Rights Reserved. * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. * This software is released under MIT license. * The full license information can be found in LICENSE in the root directory of this project. */ /** * Generated bundle index. Do not edit. */ export { CLR_FILE_MESSAGES_TEMPLATE_CONTEXT, ClrFileError, ClrFileInfo, ClrFileInput, ClrFileInputContainer, ClrFileInputModule, ClrFileInputValidator, ClrFileInputValueAccessor, ClrFileList, ClrFileMessagesTemplate, ClrFileSuccess, buildFileList, clearFiles, selectFiles }; //# sourceMappingURL=clr-angular-forms-file-input.mjs.map