@valtimo/components 13.50.0 → 13.51.0

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.
@@ -53,9 +53,10 @@ import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal';
53
53
  import { trigger, transition, style, animate } from '@angular/animations';
54
54
  import * as i1$6 from '@angular/cdk/overlay';
55
55
  import { OverlayModule, Overlay } from '@angular/cdk/overlay';
56
- import Currency from '@tadashi/currency';
56
+ import AutoNumeric from 'autonumeric';
57
57
  import flatpickr from 'flatpickr';
58
58
  import { Dutch } from 'flatpickr/dist/l10n/nl';
59
+ import createDOMPurify from 'dompurify';
59
60
  import * as i2$4 from 'ngx-spinner';
60
61
  import { NgxSpinnerModule } from 'ngx-spinner';
61
62
  import Search16 from '@carbon/icons/es/search/16';
@@ -12877,108 +12878,134 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
12877
12878
  * See the License for the specific language governing permissions and
12878
12879
  * limitations under the License.
12879
12880
  */
12881
+ const DEFAULT_LOCALE = 'nl-NL';
12882
+ const DEFAULT_CURRENCY = 'EUR';
12883
+ const RAW_VALUE_MODIFIED_EVENT = 'autoNumeric:rawValueModified';
12880
12884
  /**
12881
12885
  * Custom formio component for currency number.
12882
12886
  */
12883
12887
  class FormIoCurrencyComponent {
12884
12888
  constructor() {
12885
- this.currencyForm = new FormGroup({
12886
- currencyValue: new FormControl(''),
12887
- });
12888
12889
  this._value = null;
12889
12890
  this.valueChange = new EventEmitter();
12890
- this._subscriptions = new Subscription();
12891
+ this._disabled = false;
12892
+ this._autoNumeric = null;
12893
+ this._rendering = false;
12894
+ this._rawValueModifiedListener = () => this.onRawValueModified();
12891
12895
  }
12892
12896
  get value() {
12893
12897
  return this._value;
12894
12898
  }
12895
12899
  set value(value) {
12896
- this._value = value;
12897
- // Rendering only — emitting here would write the rendered value back over the one form.io
12898
- // is about to hand a freshly redrawn component.
12899
- this.currencyForm.setValue({
12900
- currencyValue: Currency.masking(value, this.maskOpts),
12901
- }, { emitEvent: false });
12900
+ this._value = typeof value === 'number' ? value : null;
12901
+ this.renderValue();
12902
+ }
12903
+ get disabled() {
12904
+ return this._disabled;
12902
12905
  }
12903
12906
  set disabled(value) {
12904
- if (value) {
12905
- this.currencyForm.disable({ emitEvent: false });
12907
+ this._disabled = !!value;
12908
+ }
12909
+ ngAfterViewInit() {
12910
+ const element = this.currencyElement.nativeElement;
12911
+ this._autoNumeric = new AutoNumeric(element, this.autoNumericOptions);
12912
+ element.addEventListener(RAW_VALUE_MODIFIED_EVENT, this._rawValueModifiedListener);
12913
+ this.renderValue();
12914
+ }
12915
+ ngOnChanges(changes) {
12916
+ if (this._autoNumeric &&
12917
+ (changes.currencyLocale || changes.currencyCurrency || changes.allowEmptyValue)) {
12918
+ this.withoutEmitting(() => this._autoNumeric.update(this.autoNumericOptions));
12919
+ this.renderValue();
12906
12920
  }
12907
- else {
12908
- this.currencyForm.enable({ emitEvent: false });
12921
+ }
12922
+ ngOnDestroy() {
12923
+ if (!this._autoNumeric) {
12924
+ return;
12909
12925
  }
12926
+ this.currencyElement.nativeElement.removeEventListener(RAW_VALUE_MODIFIED_EVENT, this._rawValueModifiedListener);
12927
+ this._autoNumeric.remove();
12928
+ this._autoNumeric = null;
12910
12929
  }
12911
- // A fresh object every call: Currency's constructor sets viaInput on the options it is handed.
12912
- get maskOpts() {
12930
+ /**
12931
+ * Derives the AutoNumeric formatting from the Intl formatting of the configured locale and
12932
+ * currency, so the input renders amounts exactly as that locale writes them.
12933
+ */
12934
+ get autoNumericOptions() {
12935
+ const formatter = new Intl.NumberFormat(this.currencyLocale || DEFAULT_LOCALE, {
12936
+ style: 'currency',
12937
+ currency: this.currencyCurrency || DEFAULT_CURRENCY,
12938
+ });
12939
+ const parts = formatter.formatToParts(-1234567.89);
12940
+ const partValue = (type) => parts.find(part => part.type === type)?.value ?? '';
12941
+ const indexOf = (type) => parts.findIndex(part => part.type === type);
12942
+ const currencyIndex = indexOf('currency');
12943
+ const firstIntegerIndex = indexOf('integer');
12944
+ const minusSignIndex = indexOf('minusSign');
12945
+ const currencyIsPrefix = currencyIndex < firstIntegerIndex;
12946
+ const spacing = parts[currencyIsPrefix ? currencyIndex + 1 : currencyIndex - 1];
12947
+ const literal = spacing?.type === 'literal' ? spacing.value : '';
12948
+ const currencySymbol = currencyIsPrefix
12949
+ ? `${partValue('currency')}${literal}`
12950
+ : `${literal}${partValue('currency')}`;
12951
+ const negativePositiveSignPlacement = currencyIsPrefix
12952
+ ? minusSignIndex < currencyIndex
12953
+ ? 'l'
12954
+ : 'r'
12955
+ : minusSignIndex < firstIntegerIndex
12956
+ ? 'p'
12957
+ : minusSignIndex < currencyIndex
12958
+ ? 'l'
12959
+ : 's';
12913
12960
  return {
12914
- empty: this.allowEmptyValue || false,
12915
- locales: this.currencyLocale || 'nl-NL',
12916
- digits: 2,
12917
- options: {
12918
- style: 'currency',
12919
- currency: this.currencyCurrency || 'EUR',
12920
- },
12961
+ currencySymbol,
12962
+ currencySymbolPlacement: currencyIsPrefix ? 'p' : 's',
12963
+ negativePositiveSignPlacement,
12964
+ digitGroupSeparator: partValue('group'),
12965
+ decimalCharacter: partValue('decimal') || '.',
12966
+ decimalPlaces: formatter.resolvedOptions().maximumFractionDigits,
12967
+ emptyInputBehavior: 'always',
12968
+ modifyValueOnWheel: false,
12921
12969
  };
12922
12970
  }
12923
- ngOnInit() {
12924
- this._subscriptions.add(this.currencyForm.valueChanges.subscribe(() => {
12925
- // No mask yet: the value is being rendered, not typed, so there is nothing to write back.
12926
- if (!this._currencyInstance) {
12927
- return;
12928
- }
12929
- const unmasked = this._currencyInstance.getUnmasked(this.currencyForm.value.currencyValue);
12930
- if (unmasked === 0 && this.allowEmptyValue) {
12931
- this._value = null;
12932
- this.valueChange.emit(null);
12971
+ onRawValueModified() {
12972
+ if (this._rendering || !this._autoNumeric) {
12973
+ return;
12974
+ }
12975
+ const numericString = this._autoNumeric.getNumericString();
12976
+ const isEmpty = numericString === null || numericString === '';
12977
+ const value = isEmpty ? (this.allowEmptyValue ? null : 0) : Number(numericString);
12978
+ this._value = value;
12979
+ this.valueChange.emit(value);
12980
+ }
12981
+ renderValue() {
12982
+ if (!this._autoNumeric) {
12983
+ return;
12984
+ }
12985
+ this.withoutEmitting(() => {
12986
+ if (typeof this._value === 'number') {
12987
+ this._autoNumeric.set(this._value);
12933
12988
  }
12934
12989
  else {
12935
- this._value = unmasked;
12936
- this.valueChange.emit(unmasked);
12990
+ this._autoNumeric.clear();
12937
12991
  }
12938
- }));
12939
- }
12940
- ngOnDestroy() {
12941
- this._subscriptions.unsubscribe();
12942
- }
12943
- ngAfterViewInit() {
12944
- this._currencyInstance = new Currency(this.currencyElement.nativeElement, {
12945
- maskOpts: this.maskOpts,
12946
12992
  });
12947
- // A value can arrive before the view exists, so its render is redone here with the mask in
12948
- // place.
12949
- if (typeof this._value === 'number') {
12950
- this.renderValue();
12951
- }
12952
12993
  }
12953
- ngOnChanges(changes) {
12954
- if (changes.currencyLocale || changes.currencyCurrency || changes.allowEmptyValue) {
12955
- // ngOnChanges runs before ngAfterViewInit, which is where the mask is created from the
12956
- // current inputs, so there is nothing to update yet on the first run.
12957
- if (this._currencyInstance) {
12958
- if (typeof this.currencyLocale === 'string') {
12959
- this._currencyInstance.opts.maskOpts.locales = this.currencyLocale;
12960
- }
12961
- if (typeof this.currencyCurrency === 'string') {
12962
- this._currencyInstance.opts.maskOpts.options.currency = this.currencyCurrency;
12963
- }
12964
- if (typeof this.allowEmptyValue === 'boolean') {
12965
- this._currencyInstance.opts.maskOpts.empty = this.allowEmptyValue;
12966
- }
12967
- }
12968
- this.renderValue();
12994
+ withoutEmitting(action) {
12995
+ this._rendering = true;
12996
+ try {
12997
+ action();
12998
+ }
12999
+ finally {
13000
+ this._rendering = false;
12969
13001
  }
12970
- }
12971
- renderValue() {
12972
- this.currencyForm.setValue({
12973
- currencyValue: typeof this._value === 'number' ? Currency.masking(this._value, this.maskOpts) : '',
12974
- }, { emitEvent: false });
12975
13002
  }
12976
13003
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormIoCurrencyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
12977
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: FormIoCurrencyComponent, isStandalone: false, selector: "valtimo-currency", inputs: { value: "value", disabled: "disabled", currencyLocale: "currencyLocale", currencyCurrency: "currencyCurrency", allowEmptyValue: "allowEmptyValue" }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "currencyElement", first: true, predicate: ["currencyElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"currencyForm-container\" [formGroup]=\"currencyForm\">\n <div class=\"input-group\">\n <input #currencyElement id=\"currency\" formControlName=\"currencyValue\" class=\"form-control\" />\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }] }); }
13004
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: FormIoCurrencyComponent, isStandalone: false, selector: "valtimo-currency", inputs: { value: "value", disabled: "disabled", currencyLocale: "currencyLocale", currencyCurrency: "currencyCurrency", allowEmptyValue: "allowEmptyValue" }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "currencyElement", first: true, predicate: ["currencyElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"currencyForm-container\">\n <div class=\"input-group\">\n <input\n #currencyElement\n id=\"currency\"\n class=\"form-control\"\n inputmode=\"decimal\"\n [disabled]=\"disabled\"\n />\n </div>\n</div>\n" }); }
12978
13005
  }
12979
13006
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormIoCurrencyComponent, decorators: [{
12980
13007
  type: Component,
12981
- args: [{ selector: 'valtimo-currency', standalone: false, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"currencyForm-container\" [formGroup]=\"currencyForm\">\n <div class=\"input-group\">\n <input #currencyElement id=\"currency\" formControlName=\"currencyValue\" class=\"form-control\" />\n </div>\n</div>\n" }]
13008
+ args: [{ selector: 'valtimo-currency', standalone: false, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"currencyForm-container\">\n <div class=\"input-group\">\n <input\n #currencyElement\n id=\"currency\"\n class=\"form-control\"\n inputmode=\"decimal\"\n [disabled]=\"disabled\"\n />\n </div>\n</div>\n" }]
12982
13009
  }], propDecorators: { currencyElement: [{
12983
13010
  type: ViewChild,
12984
13011
  args: ['currencyElement']
@@ -13103,6 +13130,35 @@ function registerFormioFlatpickr() {
13103
13130
  };
13104
13131
  }
13105
13132
 
13133
+ /*
13134
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
13135
+ *
13136
+ * Licensed under EUPL, Version 1.2 (the "License");
13137
+ * you may not use this file except in compliance with the License.
13138
+ * You may obtain a copy of the License at
13139
+ *
13140
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
13141
+ *
13142
+ * Unless required by applicable law or agreed to in writing, software
13143
+ * distributed under the License is distributed on an "AS IS" basis,
13144
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13145
+ * See the License for the specific language governing permissions and
13146
+ * limitations under the License.
13147
+ */
13148
+ const SANITIZE_CONFIG = {
13149
+ FORBID_TAGS: ['form', 'input', 'button', 'textarea', 'select'],
13150
+ };
13151
+ // Use an isolated DOMPurify instance so the anchor hook below does not leak onto
13152
+ // the shared singleton that formiojs (and other libraries) sanitize with.
13153
+ const purifier = createDOMPurify(window);
13154
+ purifier.addHook('afterSanitizeAttributes', node => {
13155
+ if (node.tagName === 'A') {
13156
+ node.setAttribute('target', '_blank');
13157
+ node.setAttribute('rel', 'noopener noreferrer');
13158
+ }
13159
+ });
13160
+ const sanitizeEmailHtml = (html) => purifier.sanitize(html, SANITIZE_CONFIG);
13161
+
13106
13162
  /*
13107
13163
  * Copyright 2015-2026 Ritense BV, the Netherlands.
13108
13164
  *
@@ -13121,7 +13177,7 @@ function registerFormioFlatpickr() {
13121
13177
  class FormIoMailPreviewComponent {
13122
13178
  set value(v) {
13123
13179
  this._value = v;
13124
- this.safeValue = this.sanitizer.bypassSecurityTrustHtml(v ?? 'Hier wordt het e-mailvoorbeeld getoond.');
13180
+ this.safeValue = this.sanitizer.bypassSecurityTrustHtml(sanitizeEmailHtml(v ?? 'Hier wordt het e-mailvoorbeeld getoond.'));
13125
13181
  }
13126
13182
  get value() {
13127
13183
  return this._value;