@bravobit/bb-foundation 0.53.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,12 +12,13 @@ import { BbTemplate } from '@bravobit/bb-foundation/utils';
12
12
  import * as i1$1 from '@angular/cdk/platform';
13
13
  import { Platform } from '@angular/cdk/platform';
14
14
  import * as i2 from '@bravobit/bb-foundation';
15
- import { Files, clamp, hsvToHex, hexToHsv, Exif, FileLoader, parseDate } from '@bravobit/bb-foundation';
15
+ import { Files, clamp, hsvToHex, hexToHsv, formatFileSize, Exif, FileLoader, parseDate } from '@bravobit/bb-foundation';
16
16
  import { Overlay } from '@angular/cdk/overlay';
17
17
  import { ComponentPortal } from '@angular/cdk/portal';
18
18
  import * as i3 from '@angular/platform-browser';
19
19
  import { trigger, transition, style, animate } from '@angular/animations';
20
20
  import { TooltipDirective } from '@bravobit/bb-foundation/tooltip';
21
+ import { getControlValue } from '@bravobit/bb-foundation/rxjs';
21
22
 
22
23
  class BbFormSubmit {
23
24
  _host;
@@ -775,18 +776,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
775
776
 
776
777
  class BbFileSize {
777
778
  transform(value) {
778
- return this.format(value);
779
+ return formatFileSize(value);
779
780
  }
780
- format = (value, decimals = 2) => {
781
- if (value === 0) {
782
- return '0 Bytes';
783
- }
784
- const k = 1024;
785
- const dm = decimals < 0 ? 0 : decimals;
786
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
787
- const index = Math.floor(Math.log(value) / Math.log(k));
788
- return parseFloat((value / Math.pow(k, index)).toFixed(dm)) + ' ' + sizes[index];
789
- };
790
781
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BbFileSize, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
791
782
  static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.1.6", ngImport: i0, type: BbFileSize, isStandalone: true, name: "bbFileSize" });
792
783
  }
@@ -1702,6 +1693,14 @@ function injectAcceptString() {
1702
1693
  ? null
1703
1694
  : allowedFileTypes.join(',');
1704
1695
  }
1696
+ function injectMaxFileSize() {
1697
+ const config = inject(ELEMENTS_CONFIG, { optional: true });
1698
+ return config?.maxFileSize ?? Number.MAX_SAFE_INTEGER;
1699
+ }
1700
+ function injectMaxTotalFileSize() {
1701
+ const config = inject(ELEMENTS_CONFIG, { optional: true });
1702
+ return config?.maxTotalFileSize ?? Number.MAX_SAFE_INTEGER;
1703
+ }
1705
1704
 
1706
1705
  let nextUniqueId$2 = 0;
1707
1706
  class BbMultiFileControl {
@@ -1716,6 +1715,8 @@ class BbMultiFileControl {
1716
1715
  label = null;
1717
1716
  hint = null;
1718
1717
  accept = injectAcceptString();
1718
+ maxFileSize = injectMaxFileSize();
1719
+ maxTotalFileSize = injectMaxTotalFileSize();
1719
1720
  grouped = false;
1720
1721
  required = false;
1721
1722
  disabled = false;
@@ -1764,19 +1765,32 @@ class BbMultiFileControl {
1764
1765
  onErrorChange(error) {
1765
1766
  this.error = !!error;
1766
1767
  }
1767
- validate({ value }) {
1768
- const regexString = (this.accept ?? '*')
1769
- .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
1770
- .replace(/,/g, '|');
1771
- const mimeTypeRegex = new RegExp(regexString);
1772
- const errors = (value ?? []).reduce((previous, current, index) => {
1773
- const isValid = this.isFileLike(current) && mimeTypeRegex.test(current?.type);
1774
- if (isValid) {
1768
+ validate(control) {
1769
+ const value = control?.value ?? [];
1770
+ const errors = value.reduce((previous, current, index) => {
1771
+ if (this.isValidFileType(current)) {
1775
1772
  return previous;
1776
1773
  }
1777
1774
  return { ...previous, [index]: true };
1778
1775
  }, {});
1779
- return Object.keys(errors)?.length > 0 ? { invalidFiles: errors } : null;
1776
+ if (Object.keys(errors)?.length > 0) {
1777
+ return { invalidFiles: errors };
1778
+ }
1779
+ for (const file of value) {
1780
+ if (!this.isValidFileSize(file)) {
1781
+ return { maxFileSize: { maxSize: formatFileSize(this.maxFileSize) } };
1782
+ }
1783
+ }
1784
+ const totalSize = value.reduce((previous, current) => previous + (current?.size ?? 0), 0);
1785
+ if (totalSize > this.maxTotalFileSize) {
1786
+ return {
1787
+ maxTotalFileSize: {
1788
+ max: formatFileSize(this.maxFileSize),
1789
+ current: formatFileSize(totalSize)
1790
+ }
1791
+ };
1792
+ }
1793
+ return null;
1780
1794
  }
1781
1795
  addFiles(files) {
1782
1796
  if (this.disabled) {
@@ -1791,12 +1805,31 @@ class BbMultiFileControl {
1791
1805
  }
1792
1806
  this.onChangeCallback?.(this.value);
1793
1807
  }
1808
+ isValidFileSize(file) {
1809
+ if (!this.isFileLike(file)) {
1810
+ return false;
1811
+ }
1812
+ return file?.size <= this.maxFileSize;
1813
+ }
1814
+ isValidFileType(file) {
1815
+ if (!this.isFileLike(file)) {
1816
+ return false;
1817
+ }
1818
+ if (this.accept === null || this.accept === undefined) {
1819
+ return true;
1820
+ }
1821
+ const regexString = this.accept
1822
+ .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
1823
+ .replace(/,/g, '|');
1824
+ const mimeTypeRegex = new RegExp(regexString);
1825
+ return mimeTypeRegex.test(file?.type);
1826
+ }
1794
1827
  isFileLike(input) {
1795
1828
  return 'File' in window && input instanceof File
1796
1829
  || 'Blob' in window && input instanceof Blob;
1797
1830
  }
1798
1831
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BbMultiFileControl, deps: [], target: i0.ɵɵFactoryTarget.Component });
1799
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: BbMultiFileControl, isStandalone: true, selector: "bb-multi-file-control", inputs: { label: "label", hint: "hint", accept: "accept", grouped: ["grouped", "grouped", booleanAttribute], required: ["required", "required", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], hideErrors: ["hideErrors", "hideErrors", booleanAttribute], items: "items" }, outputs: { delete: "delete" }, host: { properties: { "class.required": "required", "class.disabled": "disabled", "class.grouped": "grouped", "class.error": "error" }, classAttribute: "bb-multi-file-control" }, providers: [
1832
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: BbMultiFileControl, isStandalone: true, selector: "bb-multi-file-control", inputs: { label: "label", hint: "hint", accept: "accept", maxFileSize: ["maxFileSize", "maxFileSize", numberAttribute], maxTotalFileSize: ["maxTotalFileSize", "maxTotalFileSize", numberAttribute], grouped: ["grouped", "grouped", booleanAttribute], required: ["required", "required", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], hideErrors: ["hideErrors", "hideErrors", booleanAttribute], items: "items" }, outputs: { delete: "delete" }, host: { properties: { "class.required": "required", "class.disabled": "disabled", "class.grouped": "grouped", "class.error": "error" }, classAttribute: "bb-multi-file-control" }, providers: [
1800
1833
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BbMultiFileControl), multi: true },
1801
1834
  { provide: NG_VALIDATORS, useExisting: BbMultiFileControl, multi: true }
1802
1835
  ], viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, static: true }], ngImport: i0, template: "<!-- The label of the input. -->\n@if (label; as labelContent) {\n <label [for]=\"labelId\"\n class=\"bb-multi-file-control-label\">\n <ng-template [bbTemplate]=\"labelContent\">{{ labelContent }}</ng-template>\n </label>\n}\n\n<input #fileInput\n [id]=\"labelId\"\n [accept]=\"accept\"\n [disabled]=\"disabled\"\n (change)=\"onFileChange($event)\"\n class=\"bb-multi-file-control-input\"\n type=\"file\"\n tabindex=\"-1\"\n multiple>\n\n<div [bbFileDropDisabled]=\"disabled\"\n (bbFileDrop)=\"addFiles($event)\"\n class=\"bb-multi-file-control-container\">\n @if (showList) {\n <ul class=\"bb-multi-file-control-list\">\n @for (item of items; track item?.id) {\n <li class=\"bb-multi-file-control-item\">\n <i class=\"bb-multi-file-control-icon attach-horizontal\"></i>\n <a [href]=\"item?.url\"\n target=\"_blank\"\n rel=\"noopener\"\n class=\"bb-multi-file-control-item-content\">{{ item?.label }}</a>\n @if (!disabled && delete?.observed) {\n <button (click)=\"delete?.emit(item)\"\n type=\"button\"\n class=\"bb-multi-file-control-item-button\">\n <i class=\"bb-multi-file-control-icon clear\"></i>\n </button>\n }\n </li>\n }\n @for (file of value; track $index) {\n <li class=\"bb-multi-file-control-item\">\n <i class=\"bb-multi-file-control-icon attach-horizontal\"></i>\n <button (click)=\"downloadFile(file)\"\n class=\"bb-multi-file-control-item-content\"\n type=\"button\">\n {{ file?.name }}\n </button>\n @if (!disabled) {\n <button (click)=\"deleteFile($index)\"\n type=\"button\"\n class=\"bb-multi-file-control-item-button\">\n <i class=\"bb-multi-file-control-icon clear\"></i>\n </button>\n }\n </li>\n }\n </ul>\n } @else if (!disabled) {\n <i class=\"bb-multi-file-control-icon attach-vertical\"></i>\n <p [bb-localize-string]=\"'multi-file-control.choose_file_text' | bbLocalize\"\n class=\"bb-multi-file-control-empty\">\n <label *bbLocalizeTemplate=\"'label'\"\n [for]=\"labelId\">{{ 'multi-file-control.choose_file' | bbLocalize }}</label>\n </p>\n }\n @if (!disabled) {\n <button (click)=\"openFileDialog()\"\n type=\"button\"\n class=\"primary small bb-multi-file-control-button\"\n bb-button>\n <i class=\"bb-multi-file-control-icon add\" suffix></i>\n {{ 'multi-file-control.choose_file' | bbLocalize }}\n </button>\n }\n</div>\n\n@if (!hideErrors) {\n <bb-form-error (errorChange)=\"onErrorChange($event)\"></bb-form-error>\n}\n\n<!-- The file picker hint. -->\n@if (hint; as hintContent) {\n <p class=\"bb-multi-file-control-hint\">\n <ng-template [bbTemplate]=\"hintContent\">{{ hintContent }}</ng-template>\n </p>\n}\n", styles: [".bb-multi-file-control{display:block;line-height:normal}.bb-multi-file-control.grouped{margin-bottom:1.5rem}.bb-multi-file-control.required>.bb-multi-file-control-label:after{content:\"*\";font-size:.75rem;vertical-align:top;color:var(--bb-form-label-required-color)}.bb-multi-file-control-container.is-hovered{border-color:var(--bb-multi-file-control-focus-border-color);box-shadow:0 .375rem .375rem -.375rem #0000001a,var(--bb-multi-file-control-box-shadow)}.bb-multi-file-control.disabled>.bb-multi-file-control-container{cursor:default;box-shadow:none;color:var(--bb-control-disabled-color);background-color:var(--bb-control-disabled-background-color)}.bb-multi-file-control.error>.bb-multi-file-control-label{color:var(--bb-control-error-color)}.bb-multi-file-control.error>.bb-multi-file-control-hint{display:none}.bb-multi-file-control.error>.bb-multi-file-control-container{border:1px solid var(--bb-control-error-border-color);background-color:var(--bb-control-error-background-color)}.bb-multi-file-control-label{display:block;margin-bottom:.25rem;color:var(--bb-form-label-color);font-size:var(--bb-form-label-font-size);font-weight:var(--bb-form-label-font-weight)}.bb-multi-file-control-input{opacity:0;z-index:-1;width:.1px;height:.1px;overflow:hidden;position:absolute}.bb-multi-file-control-container{width:100%;display:flex;color:#111;padding:.5rem;overflow:hidden;min-height:7.5rem;align-items:center;border-radius:.5rem;flex-direction:column;justify-content:center;background-color:#fff;transition-duration:.25s;transition-property:background-color,box-shadow;transition-timing-function:cubic-bezier(0,0,.2,1);border:1px solid var(--bb-multi-file-control-border-color);box-shadow:0 .375rem .375rem -.375rem #0000001a}.bb-multi-file-control-button{margin-top:.5rem}.bb-multi-file-control-list{flex:1;width:100%;overflow:hidden}.bb-multi-file-control-empty{flex:1;width:100%;color:#758795;display:block;font-weight:400;line-height:1.5;text-align:center;font-size:.875rem}.bb-multi-file-control-empty>label{cursor:pointer;display:inline;font-size:inherit;font-weight:inherit;text-decoration:underline;color:var(--bb-multi-file-control-color)}.bb-multi-file-control-item{height:2rem;display:flex;max-width:100%;overflow:hidden;align-items:center;white-space:nowrap;border-radius:.5rem;text-overflow:ellipsis;padding:0 .25rem 0 .5rem;background-color:#edf4fd;border:1px solid var(--bb-multi-file-control-border-color)}.bb-multi-file-control-item:not(:last-child){margin-bottom:.25rem}.bb-multi-file-control-item-content{flex:1;padding:0;border:none;max-width:100%;margin:0 .5rem;overflow:hidden;appearance:none;text-align:left;font-weight:400;font-size:.875rem;white-space:nowrap;text-decoration:none;text-overflow:ellipsis;background-color:transparent}.bb-multi-file-control-item-content,.bb-multi-file-control-item-content:visited{color:#0a305c}.bb-multi-file-control-item-content:hover,.bb-multi-file-control-item-content:focus{text-decoration:underline}.bb-multi-file-control-item-button{padding:0;width:1.5rem;display:flex;height:1.5rem;align-items:center;border-radius:.25rem;justify-content:center;border:1px solid hsl(0,73%,25%);background-color:#b51c1c}.bb-multi-file-control-item-button:hover,.bb-multi-file-control-item-button:focus{background-color:#a81a1a}.bb-multi-file-control-item-button:active{background-color:#9a1818}.bb-multi-file-control-hint{display:block;line-height:1.5;margin-top:.25rem;font-size:.8125rem;color:#758795}.bb-multi-file-control-icon{width:1.25rem;height:1.25rem;display:inline-flex;background-size:contain;background-repeat:no-repeat;background-position:center center}.bb-multi-file-control-icon.add{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" fill=\"%23fff\" viewBox=\"0 0 24 24\"%3E%3Cpath d=\"M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"/%3E%3C/svg%3E')}.bb-multi-file-control-icon.attach-vertical{width:1.5rem;height:1.5rem;margin-bottom:.5rem;background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" fill=\"%23758795\" viewBox=\"0 0 24 24\"%3E%3Cpath d=\"M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z\"/%3E%3C/svg%3E')}.bb-multi-file-control-icon.attach-horizontal{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"%3E%3Cpath d=\"M2 12.5C2 9.46 4.46 7 7.5 7H18c2.21 0 4 1.79 4 4s-1.79 4-4 4H9.5C8.12 15 7 13.88 7 12.5S8.12 10 9.5 10H17v2H9.41c-.55 0-.55 1 0 1H18c1.1 0 2-.9 2-2s-.9-2-2-2H7.5C5.57 9 4 10.57 4 12.5S5.57 16 7.5 16H17v2H7.5C4.46 18 2 15.54 2 12.5z\"/%3E%3C/svg%3E')}.bb-multi-file-control-icon.clear{background-image:url('data:image/svg+xml,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" fill=\"%23fff\" viewBox=\"0 0 24 24\"%3E%3Cpath d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/%3E%3C/svg%3E')}\n"], dependencies: [{ kind: "component", type: BbFormError, selector: "bb-form-error", inputs: ["control"], outputs: ["errorChange"] }, { kind: "directive", type: BbTemplate, selector: "[bbTemplate]", inputs: ["bbTemplate"] }, { kind: "component", type: BbButton, selector: "button[bb-button]", inputs: ["disabled", "loading"], exportAs: ["bbButton"] }, { kind: "pipe", type: BbLocalize, name: "bbLocalize" }, { kind: "directive", type: BbLocalizeTemplate, selector: "ng-template[bbLocalizeTemplate]", inputs: ["bbLocalizeTemplate"] }, { kind: "component", type: BbLocalizeString, selector: "[bb-localize-string]", inputs: ["substitutions", "bb-localize-string"] }, { kind: "directive", type: BbFileDrop, selector: "[bbFileDrop]", inputs: ["bbFileDropDisabled"], outputs: ["bbFileDrop"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
@@ -1822,6 +1855,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
1822
1855
  type: Input
1823
1856
  }], accept: [{
1824
1857
  type: Input
1858
+ }], maxFileSize: [{
1859
+ type: Input,
1860
+ args: [{ transform: numberAttribute }]
1861
+ }], maxTotalFileSize: [{
1862
+ type: Input,
1863
+ args: [{ transform: numberAttribute }]
1825
1864
  }], grouped: [{
1826
1865
  type: Input,
1827
1866
  args: [{ transform: booleanAttribute }]
@@ -1851,6 +1890,7 @@ class BbFilePicker {
1851
1890
  label = null;
1852
1891
  hint = null;
1853
1892
  accept = injectAcceptString();
1893
+ maxFileSize = injectMaxFileSize();
1854
1894
  showImages = true;
1855
1895
  grouped = false;
1856
1896
  required = false;
@@ -1916,12 +1956,13 @@ class BbFilePicker {
1916
1956
  if (value === null || value === undefined) {
1917
1957
  return null;
1918
1958
  }
1919
- const regexString = (this.accept ?? '*')
1920
- .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
1921
- .replace(/,/g, '|');
1922
- const mimeTypeRegex = new RegExp(regexString);
1923
- const isNotValid = (typeof File === 'undefined') || !(value instanceof File) || !mimeTypeRegex.test(value?.type);
1924
- return isNotValid && { invalidFileType: true };
1959
+ if (!this.isValidFileType(value)) {
1960
+ return { invalidFileType: true };
1961
+ }
1962
+ if (!this.isValidFileSize(value)) {
1963
+ return { maxFileSize: { maxSize: formatFileSize(this.maxFileSize) } };
1964
+ }
1965
+ return null;
1925
1966
  }
1926
1967
  saveFile(files) {
1927
1968
  const file = files?.[0] ?? null;
@@ -1938,8 +1979,31 @@ class BbFilePicker {
1938
1979
  }
1939
1980
  return Array.from(element.files);
1940
1981
  }
1982
+ isValidFileType(file) {
1983
+ if (!this.isFileLike(file)) {
1984
+ return false;
1985
+ }
1986
+ if (this.accept === null || this.accept === undefined) {
1987
+ return true;
1988
+ }
1989
+ const regexString = this.accept
1990
+ .replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
1991
+ .replace(/,/g, '|');
1992
+ const mimeTypeRegex = new RegExp(regexString);
1993
+ return mimeTypeRegex.test(file?.type);
1994
+ }
1995
+ isValidFileSize(file) {
1996
+ if (!this.isFileLike(file)) {
1997
+ return false;
1998
+ }
1999
+ return file?.size <= this.maxFileSize;
2000
+ }
2001
+ isFileLike(input) {
2002
+ return 'File' in window && input instanceof File
2003
+ || 'Blob' in window && input instanceof Blob;
2004
+ }
1941
2005
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BbFilePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
1942
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: BbFilePicker, isStandalone: true, selector: "bb-file-picker", inputs: { label: "label", hint: "hint", accept: "accept", showImages: "showImages", grouped: ["grouped", "grouped", booleanAttribute], required: ["required", "required", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], hideErrors: ["hideErrors", "hideErrors", booleanAttribute], value: "value" }, outputs: { valueChange: "valueChange" }, host: { properties: { "class.required": "required", "class.disabled": "disabled", "class.grouped": "grouped", "class.error": "error" }, classAttribute: "bb-file-picker" }, providers: [
2006
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: BbFilePicker, isStandalone: true, selector: "bb-file-picker", inputs: { label: "label", hint: "hint", accept: "accept", maxFileSize: ["maxFileSize", "maxFileSize", numberAttribute], showImages: ["showImages", "showImages", booleanAttribute], grouped: ["grouped", "grouped", booleanAttribute], required: ["required", "required", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], hideErrors: ["hideErrors", "hideErrors", booleanAttribute], value: "value" }, outputs: { valueChange: "valueChange" }, host: { properties: { "class.required": "required", "class.disabled": "disabled", "class.grouped": "grouped", "class.error": "error" }, classAttribute: "bb-file-picker" }, providers: [
1943
2007
  {
1944
2008
  provide: NG_VALUE_ACCESSOR,
1945
2009
  useExisting: forwardRef(() => BbFilePicker),
@@ -2004,8 +2068,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
2004
2068
  type: Input
2005
2069
  }], accept: [{
2006
2070
  type: Input
2071
+ }], maxFileSize: [{
2072
+ type: Input,
2073
+ args: [{ transform: numberAttribute }]
2007
2074
  }], showImages: [{
2008
- type: Input
2075
+ type: Input,
2076
+ args: [{ transform: booleanAttribute }]
2009
2077
  }], grouped: [{
2010
2078
  type: Input,
2011
2079
  args: [{ transform: booleanAttribute }]
@@ -2569,6 +2637,161 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
2569
2637
  type: Output
2570
2638
  }] } });
2571
2639
 
2640
+ class BbPincodeControl {
2641
+ // Dependencies.
2642
+ _platform = inject(Platform);
2643
+ _changeDetectorRef = inject(ChangeDetectorRef);
2644
+ // Views.
2645
+ htmlInputElementRef;
2646
+ // Inputs.
2647
+ label = null;
2648
+ hint = null;
2649
+ placeholder = null;
2650
+ autoFocus = false;
2651
+ required = false;
2652
+ readonly = false;
2653
+ grouped = false;
2654
+ digits = 6;
2655
+ get value() {
2656
+ return this.control?.value;
2657
+ }
2658
+ valueChange = new EventEmitter();
2659
+ set value(value) {
2660
+ this.control?.setValue(value);
2661
+ this.valueChange.emit(value);
2662
+ }
2663
+ // Data.
2664
+ data$;
2665
+ // State.
2666
+ control = new FormControl(null);
2667
+ error = false;
2668
+ // Callbacks.
2669
+ onTouchedCallback = () => ({});
2670
+ onChangeCallback = () => ({});
2671
+ // Subscriptions.
2672
+ _subscription = new Subscription();
2673
+ ngOnInit() {
2674
+ this.setData();
2675
+ this.handleValueChanges();
2676
+ this.handleAutoFocus();
2677
+ }
2678
+ ngOnDestroy() {
2679
+ this._subscription?.unsubscribe();
2680
+ }
2681
+ writeValue(value) {
2682
+ this.value = value;
2683
+ }
2684
+ registerOnChange(method) {
2685
+ this.onChangeCallback = method;
2686
+ }
2687
+ registerOnTouched(method) {
2688
+ this.onTouchedCallback = method;
2689
+ }
2690
+ setDisabledState(isDisabled) {
2691
+ isDisabled
2692
+ ? this.control?.disable()
2693
+ : this.control?.enable();
2694
+ this._changeDetectorRef.markForCheck();
2695
+ }
2696
+ onErrorChange(error) {
2697
+ this.error = !!error;
2698
+ }
2699
+ validate(control) {
2700
+ const value = control?.value;
2701
+ if (value === null || value === undefined) {
2702
+ return null;
2703
+ }
2704
+ if (value?.length > this.digits) {
2705
+ return {
2706
+ maxlength: {
2707
+ actualLength: value?.length ?? 0,
2708
+ requiredLength: this.digits ?? 0
2709
+ }
2710
+ };
2711
+ }
2712
+ if (value?.length < this.digits) {
2713
+ return {
2714
+ minlength: {
2715
+ actualLength: value?.length ?? 0,
2716
+ requiredLength: this.digits ?? 0
2717
+ }
2718
+ };
2719
+ }
2720
+ return null;
2721
+ }
2722
+ setData() {
2723
+ this.data$ = getControlValue(this.control).pipe(map(value => value ?? ''), map(value => {
2724
+ const items = Array(this.digits)
2725
+ .fill(0)
2726
+ .map((_, index, array) => ({
2727
+ index: index,
2728
+ value: value?.[index] ?? '',
2729
+ active: Math.min(array?.length - 1, value?.length) === index
2730
+ }));
2731
+ return { items };
2732
+ }));
2733
+ }
2734
+ handleValueChanges() {
2735
+ const subscription = this.control.valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
2736
+ this.valueChange.emit(this.value);
2737
+ this.onChangeCallback(this.value);
2738
+ });
2739
+ this._subscription.add(subscription);
2740
+ }
2741
+ handleAutoFocus() {
2742
+ if (!this.autoFocus || !this._platform.isBrowser) {
2743
+ return;
2744
+ }
2745
+ setTimeout(() => this.htmlInputElementRef?.nativeElement?.focus?.(), 0);
2746
+ }
2747
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BbPincodeControl, deps: [], target: i0.ɵɵFactoryTarget.Component });
2748
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.6", type: BbPincodeControl, isStandalone: true, selector: "bb-pincode-control", inputs: { label: "label", hint: "hint", placeholder: "placeholder", autoFocus: ["autoFocus", "autoFocus", booleanAttribute], required: ["required", "required", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute], grouped: ["grouped", "grouped", booleanAttribute], digits: ["digits", "digits", numberAttribute], value: "value" }, outputs: { valueChange: "valueChange" }, host: { properties: { "class.required": "required", "class.readonly": "readonly", "class.grouped": "grouped", "class.error": "error" }, classAttribute: "bb-pincode-control" }, providers: [
2749
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BbPincodeControl), multi: true },
2750
+ { provide: NG_VALIDATORS, useExisting: BbPincodeControl, multi: true }
2751
+ ], viewQueries: [{ propertyName: "htmlInputElementRef", first: true, predicate: ["htmlInputElement"], descendants: true, static: true }], ngImport: i0, template: "<!-- The label of the input. -->\n@if (label; as labelContent) {\n <label class=\"bb-pincode-control-label\">\n <ng-template [bbTemplate]=\"labelContent\">{{ labelContent }}</ng-template>\n </label>\n}\n\n<input #htmlInputElement\n [formControl]=\"control\"\n [minlength]=\"digits\"\n [maxlength]=\"digits\"\n [readonly]=\"readonly\"\n inputmode=\"numeric\"\n tabindex=\"-1\"\n autocomplete=\"off\"\n class=\"bb-pincode-control-element\"\n type=\"text\">\n\n@if (data$ | async; as data) {\n <div class=\"bb-pincode-control-container\">\n @for (item of data?.items; track item?.index) {\n <div [class.active]=\"item?.active\"\n [class.has-value]=\"!!item?.value\"\n [attr.data-placeholder]=\"placeholder\"\n (click)=\"htmlInputElement?.focus()\"\n class=\"bb-pincode-control-cell\">\n {{ item?.value }}\n </div>\n }\n </div>\n}\n\n<bb-form-error (errorChange)=\"onErrorChange($event)\"></bb-form-error>\n\n<!-- The file picker hint. -->\n@if (hint; as hintContent) {\n <p class=\"bb-pincode-control-hint\">\n <ng-template [bbTemplate]=\"hintContent\">{{ hintContent }}</ng-template>\n </p>\n}\n", styles: [".bb-pincode-control{display:block;line-height:normal}.bb-pincode-control.grouped{margin-bottom:1.5rem}.bb-pincode-control.required>.bb-pincode-control-label:after{content:\"*\";color:#c23934;font-size:.75rem;vertical-align:top}.bb-pincode-control.error>.bb-pincode-control-label{color:var(--bb-control-error-color)}.bb-pincode-control.error>.bb-pincode-control-hint{display:none}.bb-pincode-control.error>.bb-pincode-control-container>.bb-pincode-control-cell{border:1px solid var(--bb-control-error-border-color);background-color:var(--bb-control-error-background-color)}.bb-pincode-control.error>.bb-pincode-control-container>.bb-pincode-control-cell:not(.has-value):before{color:var(--bb-control-error-placeholder-color)}.bb-pincode-control.error>.bb-pincode-control-element:not(:disabled):not(:read-only):focus+.bb-pincode-control-container>.bb-pincode-control-cell.active{box-shadow:var(--bb-control-error-box-shadow)}.bb-pincode-control-element:not(:disabled):read-only+.bb-pincode-control-container>.bb-pincode-control-cell{cursor:default;box-shadow:none;border-style:dotted;border-color:#bdc4c9;background-color:transparent}.bb-pincode-control-element:disabled+.bb-pincode-control-container>.bb-pincode-control-cell{cursor:default;box-shadow:none;pointer-events:none;color:var(--bb-control-disabled-color);background-color:var(--bb-control-disabled-background-color)}.bb-pincode-control-element{opacity:0;z-index:-1;width:.1px;height:.1px;overflow:hidden;position:absolute}.bb-pincode-control-element:not(:disabled):not(:read-only):focus+.bb-pincode-control-container>.bb-pincode-control-cell.active{box-shadow:var(--bb-form-control-focus-box-shadow);border-color:var(--bb-form-control-focus-border-color)}.bb-pincode-control-element:not(:disabled):not(:read-only):focus+.bb-pincode-control-container>.bb-pincode-control-cell.active:not(.has-value):after{top:50%;content:\"|\";position:absolute;text-align:center;transform:translate(-25%) translateY(-50%);animation:bbCursorBlink .8s step-end infinite}.bb-pincode-control-container{display:flex;gap:.75rem;align-items:center}.bb-pincode-control-cell{flex:1;cursor:text;height:3rem;display:flex;font-size:1rem;-webkit-user-select:none;user-select:none;text-align:center;position:relative;align-items:center;justify-content:center;color:var(--text-color);box-shadow:var(--bb-form-control-box-shadow);border:1px solid var(--bb-form-control-border-color);background-color:var(--bb-form-control-background-color);border-radius:var(--bb-form-control-border-radius, .5rem);transition:background-color .25s cubic-bezier(0,0,.2,1),box-shadow .25s cubic-bezier(0,0,.2,1)}.bb-pincode-control-cell:not(.has-value):before{top:0;bottom:0;left:.25rem;right:.25rem;display:block;max-width:100%;overflow:hidden;line-height:3rem;position:absolute;text-align:center;white-space:nowrap;vertical-align:middle;text-overflow:ellipsis;color:#d1d1d1;content:attr(data-placeholder)}.bb-pincode-control-label{color:#525252;display:block;font-weight:400;font-size:.875rem;margin-bottom:.25rem}.bb-pincode-control-hint{display:block;line-height:1.5;margin-top:.25rem;font-size:.8125rem;color:#758795}@keyframes bbCursorBlink{0%{opacity:1}50%{opacity:0}to{opacity:1}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: BbTemplate, selector: "[bbTemplate]", inputs: ["bbTemplate"] }, { kind: "component", type: BbFormError, selector: "bb-form-error", inputs: ["control"], outputs: ["errorChange"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2752
+ }
2753
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: BbPincodeControl, decorators: [{
2754
+ type: Component,
2755
+ args: [{ selector: 'bb-pincode-control', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, providers: [
2756
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => BbPincodeControl), multi: true },
2757
+ { provide: NG_VALIDATORS, useExisting: BbPincodeControl, multi: true }
2758
+ ], host: {
2759
+ 'class': 'bb-pincode-control',
2760
+ '[class.required]': 'required',
2761
+ '[class.readonly]': 'readonly',
2762
+ '[class.grouped]': 'grouped',
2763
+ '[class.error]': 'error'
2764
+ }, imports: [ReactiveFormsModule, BbTemplate, BbFormError, AsyncPipe], template: "<!-- The label of the input. -->\n@if (label; as labelContent) {\n <label class=\"bb-pincode-control-label\">\n <ng-template [bbTemplate]=\"labelContent\">{{ labelContent }}</ng-template>\n </label>\n}\n\n<input #htmlInputElement\n [formControl]=\"control\"\n [minlength]=\"digits\"\n [maxlength]=\"digits\"\n [readonly]=\"readonly\"\n inputmode=\"numeric\"\n tabindex=\"-1\"\n autocomplete=\"off\"\n class=\"bb-pincode-control-element\"\n type=\"text\">\n\n@if (data$ | async; as data) {\n <div class=\"bb-pincode-control-container\">\n @for (item of data?.items; track item?.index) {\n <div [class.active]=\"item?.active\"\n [class.has-value]=\"!!item?.value\"\n [attr.data-placeholder]=\"placeholder\"\n (click)=\"htmlInputElement?.focus()\"\n class=\"bb-pincode-control-cell\">\n {{ item?.value }}\n </div>\n }\n </div>\n}\n\n<bb-form-error (errorChange)=\"onErrorChange($event)\"></bb-form-error>\n\n<!-- The file picker hint. -->\n@if (hint; as hintContent) {\n <p class=\"bb-pincode-control-hint\">\n <ng-template [bbTemplate]=\"hintContent\">{{ hintContent }}</ng-template>\n </p>\n}\n", styles: [".bb-pincode-control{display:block;line-height:normal}.bb-pincode-control.grouped{margin-bottom:1.5rem}.bb-pincode-control.required>.bb-pincode-control-label:after{content:\"*\";color:#c23934;font-size:.75rem;vertical-align:top}.bb-pincode-control.error>.bb-pincode-control-label{color:var(--bb-control-error-color)}.bb-pincode-control.error>.bb-pincode-control-hint{display:none}.bb-pincode-control.error>.bb-pincode-control-container>.bb-pincode-control-cell{border:1px solid var(--bb-control-error-border-color);background-color:var(--bb-control-error-background-color)}.bb-pincode-control.error>.bb-pincode-control-container>.bb-pincode-control-cell:not(.has-value):before{color:var(--bb-control-error-placeholder-color)}.bb-pincode-control.error>.bb-pincode-control-element:not(:disabled):not(:read-only):focus+.bb-pincode-control-container>.bb-pincode-control-cell.active{box-shadow:var(--bb-control-error-box-shadow)}.bb-pincode-control-element:not(:disabled):read-only+.bb-pincode-control-container>.bb-pincode-control-cell{cursor:default;box-shadow:none;border-style:dotted;border-color:#bdc4c9;background-color:transparent}.bb-pincode-control-element:disabled+.bb-pincode-control-container>.bb-pincode-control-cell{cursor:default;box-shadow:none;pointer-events:none;color:var(--bb-control-disabled-color);background-color:var(--bb-control-disabled-background-color)}.bb-pincode-control-element{opacity:0;z-index:-1;width:.1px;height:.1px;overflow:hidden;position:absolute}.bb-pincode-control-element:not(:disabled):not(:read-only):focus+.bb-pincode-control-container>.bb-pincode-control-cell.active{box-shadow:var(--bb-form-control-focus-box-shadow);border-color:var(--bb-form-control-focus-border-color)}.bb-pincode-control-element:not(:disabled):not(:read-only):focus+.bb-pincode-control-container>.bb-pincode-control-cell.active:not(.has-value):after{top:50%;content:\"|\";position:absolute;text-align:center;transform:translate(-25%) translateY(-50%);animation:bbCursorBlink .8s step-end infinite}.bb-pincode-control-container{display:flex;gap:.75rem;align-items:center}.bb-pincode-control-cell{flex:1;cursor:text;height:3rem;display:flex;font-size:1rem;-webkit-user-select:none;user-select:none;text-align:center;position:relative;align-items:center;justify-content:center;color:var(--text-color);box-shadow:var(--bb-form-control-box-shadow);border:1px solid var(--bb-form-control-border-color);background-color:var(--bb-form-control-background-color);border-radius:var(--bb-form-control-border-radius, .5rem);transition:background-color .25s cubic-bezier(0,0,.2,1),box-shadow .25s cubic-bezier(0,0,.2,1)}.bb-pincode-control-cell:not(.has-value):before{top:0;bottom:0;left:.25rem;right:.25rem;display:block;max-width:100%;overflow:hidden;line-height:3rem;position:absolute;text-align:center;white-space:nowrap;vertical-align:middle;text-overflow:ellipsis;color:#d1d1d1;content:attr(data-placeholder)}.bb-pincode-control-label{color:#525252;display:block;font-weight:400;font-size:.875rem;margin-bottom:.25rem}.bb-pincode-control-hint{display:block;line-height:1.5;margin-top:.25rem;font-size:.8125rem;color:#758795}@keyframes bbCursorBlink{0%{opacity:1}50%{opacity:0}to{opacity:1}}\n"] }]
2765
+ }], propDecorators: { htmlInputElementRef: [{
2766
+ type: ViewChild,
2767
+ args: ['htmlInputElement', { static: true }]
2768
+ }], label: [{
2769
+ type: Input
2770
+ }], hint: [{
2771
+ type: Input
2772
+ }], placeholder: [{
2773
+ type: Input
2774
+ }], autoFocus: [{
2775
+ type: Input,
2776
+ args: [{ transform: booleanAttribute }]
2777
+ }], required: [{
2778
+ type: Input,
2779
+ args: [{ transform: booleanAttribute }]
2780
+ }], readonly: [{
2781
+ type: Input,
2782
+ args: [{ transform: booleanAttribute }]
2783
+ }], grouped: [{
2784
+ type: Input,
2785
+ args: [{ transform: booleanAttribute }]
2786
+ }], digits: [{
2787
+ type: Input,
2788
+ args: [{ transform: numberAttribute }]
2789
+ }], value: [{
2790
+ type: Input
2791
+ }], valueChange: [{
2792
+ type: Output
2793
+ }] } });
2794
+
2572
2795
  function provideElementsConfig(config) {
2573
2796
  const providers = [
2574
2797
  { provide: ELEMENTS_CONFIG, useValue: config },
@@ -2587,6 +2810,7 @@ function provideElementsConfig(config) {
2587
2810
  min: ({ min }) => ({ token: 'form-control-errors.min', data: { min } }),
2588
2811
  max: ({ max }) => ({ token: 'form-control-errors.max', data: { max } }),
2589
2812
  maxFileSize: ({ maxSize }) => ({ token: 'form-control-errors.maxFileSize', data: { maxSize } }),
2813
+ maxTotalFileSize: ({ max, current }) => ({ token: 'form-control-errors.maxTotalFileSize', data: { max, current } }),
2590
2814
  minDate: ({ date }) => ({ token: 'form-control-errors.minDate', data: { date } }),
2591
2815
  maxDate: ({ date }) => ({ token: 'form-control-errors.maxDate', data: { date } }),
2592
2816
  invalidDate: () => 'form-control-errors.invalidDate',
@@ -2654,7 +2878,8 @@ const IMPORTS_EXPORTS = [
2654
2878
  BbDatePicker,
2655
2879
  BbColorSlider,
2656
2880
  BbColorPicker,
2657
- BbImageControl
2881
+ BbImageControl,
2882
+ BbPincodeControl
2658
2883
  ];
2659
2884
  class ElementsModule {
2660
2885
  static forRoot(config) {
@@ -2699,7 +2924,8 @@ class ElementsModule {
2699
2924
  BbDatePicker,
2700
2925
  BbColorSlider,
2701
2926
  BbColorPicker,
2702
- BbImageControl], exports: [
2927
+ BbImageControl,
2928
+ BbPincodeControl], exports: [
2703
2929
  // Directives.
2704
2930
  BbFormError,
2705
2931
  BbFormSubmit,
@@ -2732,9 +2958,11 @@ class ElementsModule {
2732
2958
  BbDatePicker,
2733
2959
  BbColorSlider,
2734
2960
  BbColorPicker,
2735
- BbImageControl] });
2961
+ BbImageControl,
2962
+ BbPincodeControl] });
2736
2963
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: ElementsModule, imports: [BbMultiFileControl,
2737
- BbDatePicker] });
2964
+ BbDatePicker,
2965
+ BbPincodeControl] });
2738
2966
  }
2739
2967
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: ElementsModule, decorators: [{
2740
2968
  type: NgModule,
@@ -2748,5 +2976,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.6", ngImpor
2748
2976
  * Generated bundle index. Do not edit.
2749
2977
  */
2750
2978
 
2751
- export { BbAnchor, BbAvatar, BbButton, BbCheckbox, BbCheckboxGroup, BbColorPicker, BbColorSlider, BbDate, BbDatePicker, BbExtraErrorControls, BbFileDataUrl, BbFileDrop, BbFileImage, BbFilePicker, BbFileSize, BbFormControl, BbFormError, BbFormGroup, BbFormSubmit, BbFormSubmitter, BbIcon, BbImageControl, BbImagePicker, BbImageUpload, BbInput, BbMultiFileControl, BbRadioButton, BbRadioGroup, BbRelativeTime, BbSpinner, ColorPickerDirective, ELEMENTS_CONFIG, ELEMENTS_ERRORS, ELEMENTS_ICONS, ElementsModule, injectAcceptString, provideElementsConfig };
2979
+ export { BbAnchor, BbAvatar, BbButton, BbCheckbox, BbCheckboxGroup, BbColorPicker, BbColorSlider, BbDate, BbDatePicker, BbExtraErrorControls, BbFileDataUrl, BbFileDrop, BbFileImage, BbFilePicker, BbFileSize, BbFormControl, BbFormError, BbFormGroup, BbFormSubmit, BbFormSubmitter, BbIcon, BbImageControl, BbImagePicker, BbImageUpload, BbInput, BbMultiFileControl, BbPincodeControl, BbRadioButton, BbRadioGroup, BbRelativeTime, BbSpinner, ColorPickerDirective, ELEMENTS_CONFIG, ELEMENTS_ERRORS, ELEMENTS_ICONS, ElementsModule, injectAcceptString, injectMaxFileSize, injectMaxTotalFileSize, provideElementsConfig };
2752
2980
  //# sourceMappingURL=bravobit-bb-foundation-elements.mjs.map