@yourself.create/ngx-form-designer 0.0.7 → 0.0.8

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.
@@ -5257,7 +5257,7 @@ class JsonFormRendererComponent {
5257
5257
  this.runtimeFieldDataAccessRegistry.unregister(this.engine);
5258
5258
  }
5259
5259
  async emitValuePayload(values) {
5260
- const fieldValueMap = await this.buildFieldValueMap(values);
5260
+ const fieldValueMap = await this.buildFieldValueMap(values, { normalizeFileFieldsForSubmit: true });
5261
5261
  const groupedValues = this.buildGroupedValues(fieldValueMap);
5262
5262
  const combinedValues = this.buildCombinedValues(fieldValueMap);
5263
5263
  this.valueChange.emit(fieldValueMap);
@@ -5419,16 +5419,10 @@ class JsonFormRendererComponent {
5419
5419
  this.validationChange.emit(validation);
5420
5420
  // Notify all widgets to show errors if any
5421
5421
  this.engine?.submit();
5422
- const preUploadFieldValueMap = this.uploadOnSubmit
5423
- ? await this.buildFieldValueMap(this.engine?.getValues() ?? {}, { normalizeFileFieldsForSubmit: true })
5424
- : undefined;
5425
5422
  const uploadedFiles = this.uploadOnSubmit ? await this.uploadPendingFiles() : {};
5426
5423
  this.uploadedFilesChange.emit(uploadedFiles);
5427
5424
  const values = this.engine?.getValues() ?? {};
5428
- const fieldValueMap = await this.buildFieldValueMap(values, { normalizeFileFieldsForSubmit: true });
5429
- const submitValues = preUploadFieldValueMap
5430
- ? this.mergeFileMetadata(fieldValueMap, preUploadFieldValueMap)
5431
- : fieldValueMap;
5425
+ const submitValues = await this.buildFieldValueMap(values, { normalizeFileFieldsForSubmit: true });
5432
5426
  this.formSubmit.emit({
5433
5427
  values: submitValues,
5434
5428
  groupedValues: this.buildGroupedValues(submitValues),
@@ -5689,13 +5683,14 @@ class JsonFormRendererComponent {
5689
5683
  continue;
5690
5684
  }
5691
5685
  if (field.type === 'file') {
5686
+ const normalizeFileFieldsForSubmit = options.normalizeFileFieldsForSubmit === true;
5692
5687
  const fileValue = {
5693
5688
  fieldName: field.name,
5694
- fieldValue: options.normalizeFileFieldsForSubmit
5695
- ? this.normalizeFileFieldSubmitValue(rawValue)
5689
+ fieldValue: normalizeFileFieldsForSubmit
5690
+ ? await this.buildFileSubmitValueEntries(rawValue)
5696
5691
  : rawValue,
5697
- ...(options.normalizeFileFieldsForSubmit ? { fieldType: field.type } : {}),
5698
- ...(await this.buildFileFieldMetadata(rawValue))
5692
+ ...(normalizeFileFieldsForSubmit ? { fieldType: field.type } : {}),
5693
+ ...(!normalizeFileFieldsForSubmit ? await this.buildFileFieldMetadata(rawValue) : {})
5699
5694
  };
5700
5695
  mapped[field.id] = fileValue;
5701
5696
  continue;
@@ -5782,7 +5777,12 @@ class JsonFormRendererComponent {
5782
5777
  return undefined;
5783
5778
  return values.length === 1 ? values[0] : values;
5784
5779
  }
5785
- normalizeFileFieldSubmitValue(value) {
5780
+ async buildFileSubmitValueEntries(value) {
5781
+ const rawEntries = this.normalizeFileFieldEntries(value);
5782
+ const normalizedEntries = await Promise.all(rawEntries.map(entry => this.normalizeSingleFileFieldEntry(entry)));
5783
+ return normalizedEntries.filter((entry) => entry !== null);
5784
+ }
5785
+ normalizeFileFieldEntries(value) {
5786
5786
  if (this.isFileList(value)) {
5787
5787
  return Array.from(value);
5788
5788
  }
@@ -5794,6 +5794,23 @@ class JsonFormRendererComponent {
5794
5794
  }
5795
5795
  return [value];
5796
5796
  }
5797
+ async normalizeSingleFileFieldEntry(value) {
5798
+ if (this.isFile(value)) {
5799
+ return this.buildPendingSubmitFileValue(value);
5800
+ }
5801
+ if (this.isUploadedFileRef(value)) {
5802
+ return { ...value };
5803
+ }
5804
+ return null;
5805
+ }
5806
+ async buildPendingSubmitFileValue(file) {
5807
+ return {
5808
+ name: file.name || undefined,
5809
+ size: Number.isFinite(file.size) ? file.size : undefined,
5810
+ type: file.type || undefined,
5811
+ data: await this.readFileBase64(file)
5812
+ };
5813
+ }
5797
5814
  getUploadedFileRefs(value) {
5798
5815
  if (this.isUploadedFileRef(value))
5799
5816
  return [value];
@@ -5808,26 +5825,6 @@ class JsonFormRendererComponent {
5808
5825
  : undefined;
5809
5826
  return fieldLabel === undefined ? {} : { fieldLabel };
5810
5827
  }
5811
- mergeFileMetadata(target, source) {
5812
- const merged = { ...target };
5813
- for (const fieldId of Object.keys(source)) {
5814
- const sourceValue = source[fieldId];
5815
- const targetValue = merged[fieldId];
5816
- if (!sourceValue || !targetValue)
5817
- continue;
5818
- if (sourceValue.fileType === undefined && sourceValue.data === undefined)
5819
- continue;
5820
- const nextValue = { ...targetValue };
5821
- if (sourceValue.fileType !== undefined) {
5822
- nextValue.fileType = sourceValue.fileType;
5823
- }
5824
- if (sourceValue.data !== undefined) {
5825
- nextValue.data = sourceValue.data;
5826
- }
5827
- merged[fieldId] = nextValue;
5828
- }
5829
- return merged;
5830
- }
5831
5828
  buildGroupedValues(values) {
5832
5829
  const grouped = {};
5833
5830
  const schema = this.engine?.getSchema();
@@ -15891,8 +15888,33 @@ class DataPanelComponent {
15891
15888
  showOptionMappingControls() {
15892
15889
  return this.sourceType === 'source' && this.widgetType !== 'table' && this.usesOptionMapping();
15893
15890
  }
15894
- showOptionLabelFormattingControls() {
15895
- return this.widgetType === 'select' && this.usesOptionMapping();
15891
+ showDisplayFormattingControls() {
15892
+ if (this.widgetType === 'select' && this.usesOptionMapping()) {
15893
+ return true;
15894
+ }
15895
+ return this.sourceType === 'source'
15896
+ && this.bindingShape === 'scalar'
15897
+ && (this.widgetType === 'text' || this.widgetType === 'number');
15898
+ }
15899
+ supportsNumericDisplayFormatting() {
15900
+ return this.widgetType === 'select' || this.widgetType === 'number';
15901
+ }
15902
+ displayFormattingTitle() {
15903
+ return this.widgetType === 'select' ? 'Amount Display' : 'Field Display';
15904
+ }
15905
+ displayFormattingDescription() {
15906
+ if (this.widgetType === 'select') {
15907
+ return 'Format the visible dropdown label without changing the stored option value.';
15908
+ }
15909
+ if (this.widgetType === 'number') {
15910
+ return 'Show a fixed prefix and grouped digits in the visible field value without changing the stored number.';
15911
+ }
15912
+ return 'Show a fixed prefix beside the editable value without changing the stored field value.';
15913
+ }
15914
+ numericDisplayFormattingLabel() {
15915
+ return this.widgetType === 'select'
15916
+ ? 'Show thousand separators for numeric labels'
15917
+ : 'Show thousand separators for numeric values';
15896
15918
  }
15897
15919
  showStaticOptionsEditor() {
15898
15920
  return this.sourceType === 'static' && this.widgetType !== 'table' && this.usesOptionMapping();
@@ -15945,7 +15967,12 @@ class DataPanelComponent {
15945
15967
  return sample ? collectArrayPaths(sample) : [];
15946
15968
  }
15947
15969
  shouldPersistOptionLabelFormatting() {
15948
- return this.widgetType === 'select' && this.usesOptionMapping();
15970
+ if (this.widgetType === 'select' && this.usesOptionMapping()) {
15971
+ return true;
15972
+ }
15973
+ return this.sourceType === 'source'
15974
+ && this.bindingShape === 'scalar'
15975
+ && (this.widgetType === 'text' || this.widgetType === 'number');
15949
15976
  }
15950
15977
  usesOptionMapping() {
15951
15978
  return this.bindingShape === 'list' || this.widgetType === 'search';
@@ -16266,17 +16293,17 @@ class DataPanelComponent {
16266
16293
  </div>
16267
16294
  </div>
16268
16295
 
16269
- <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showOptionLabelFormattingControls()">
16270
- <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Amount Display</div>
16271
- <div class="mt-1 text-[11px] text-gray-500">Format the visible dropdown label without changing the stored option value.</div>
16296
+ <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showDisplayFormattingControls()">
16297
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ displayFormattingTitle() }}</div>
16298
+ <div class="mt-1 text-[11px] text-gray-500">{{ displayFormattingDescription() }}</div>
16272
16299
 
16273
- <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
16300
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700" *ngIf="supportsNumericDisplayFormatting()">
16274
16301
  <input
16275
16302
  type="checkbox"
16276
16303
  [checked]="formatNumericOptionLabels"
16277
16304
  (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16278
16305
  class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16279
- <span>Show thousand separators for numeric labels</span>
16306
+ <span>{{ numericDisplayFormattingLabel() }}</span>
16280
16307
  </label>
16281
16308
 
16282
16309
  <div class="mt-3 flex flex-col gap-1">
@@ -16885,17 +16912,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
16885
16912
  </div>
16886
16913
  </div>
16887
16914
 
16888
- <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showOptionLabelFormattingControls()">
16889
- <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Amount Display</div>
16890
- <div class="mt-1 text-[11px] text-gray-500">Format the visible dropdown label without changing the stored option value.</div>
16915
+ <div class="rounded-lg border border-gray-200 bg-white p-3" *ngIf="showDisplayFormattingControls()">
16916
+ <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ displayFormattingTitle() }}</div>
16917
+ <div class="mt-1 text-[11px] text-gray-500">{{ displayFormattingDescription() }}</div>
16891
16918
 
16892
- <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
16919
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700" *ngIf="supportsNumericDisplayFormatting()">
16893
16920
  <input
16894
16921
  type="checkbox"
16895
16922
  [checked]="formatNumericOptionLabels"
16896
16923
  (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16897
16924
  class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16898
- <span>Show thousand separators for numeric labels</span>
16925
+ <span>{{ numericDisplayFormattingLabel() }}</span>
16899
16926
  </label>
16900
16927
 
16901
16928
  <div class="mt-3 flex flex-col gap-1">
@@ -17674,11 +17701,18 @@ class RulesPanelComponent {
17674
17701
  ruleActionOptions = [
17675
17702
  { label: 'Visible', value: 'visible' },
17676
17703
  { label: 'Hidden', value: 'hidden' },
17677
- { label: 'Enabled', value: 'enable' },
17678
17704
  { label: 'Disabled', value: 'disable' },
17679
17705
  { label: 'Required', value: 'required' },
17680
17706
  { label: 'Optional', value: 'optional' }
17681
17707
  ];
17708
+ actionLabels = {
17709
+ visible: 'Visible',
17710
+ hidden: 'Hidden',
17711
+ enable: 'Enabled',
17712
+ disable: 'Disabled',
17713
+ required: 'Required',
17714
+ optional: 'Optional'
17715
+ };
17682
17716
  severityOptions = [
17683
17717
  { label: 'Warn', value: 'warn' },
17684
17718
  { label: 'Error', value: 'error' }
@@ -17730,7 +17764,7 @@ class RulesPanelComponent {
17730
17764
  return `${primary} / else ${this.getActionLabel(rule.elseAction)}`;
17731
17765
  }
17732
17766
  getActionLabel(action) {
17733
- return this.ruleActionOptions.find((option) => option.value === action)?.label ?? action;
17767
+ return this.actionLabels[action] ?? action;
17734
17768
  }
17735
17769
  getActionColor(action) {
17736
17770
  switch (action) {
@@ -17749,7 +17783,7 @@ class RulesPanelComponent {
17749
17783
  <div class="flex items-center justify-between mb-2">
17750
17784
  <div class="flex flex-col">
17751
17785
  <span class="font-semibold text-gray-700">Rules</span>
17752
- <span class="text-xs text-gray-500">Rules can override visibility, enabled, and required set elsewhere.</span>
17786
+ <span class="text-xs text-gray-500">Rules can override visibility, disabled, and required state set elsewhere.</span>
17753
17787
  </div>
17754
17788
  <button (click)="addRule()"
17755
17789
  [disabled]="readOnly"
@@ -17871,7 +17905,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17871
17905
  <div class="flex items-center justify-between mb-2">
17872
17906
  <div class="flex flex-col">
17873
17907
  <span class="font-semibold text-gray-700">Rules</span>
17874
- <span class="text-xs text-gray-500">Rules can override visibility, enabled, and required set elsewhere.</span>
17908
+ <span class="text-xs text-gray-500">Rules can override visibility, disabled, and required state set elsewhere.</span>
17875
17909
  </div>
17876
17910
  <button (click)="addRule()"
17877
17911
  [disabled]="readOnly"
@@ -19546,34 +19580,35 @@ class FormPreviewComponent {
19546
19580
  copyData() {
19547
19581
  navigator.clipboard.writeText(JSON.stringify(this.previewData(), null, 2));
19548
19582
  }
19549
- sanitizePreviewData(value) {
19583
+ sanitizePreviewData(value, withinFileField = false) {
19550
19584
  if (Array.isArray(value)) {
19551
- return value.map(item => this.sanitizePreviewData(item));
19585
+ return value.map(item => this.sanitizePreviewData(item, withinFileField));
19552
19586
  }
19553
19587
  if (!this.isObjectRecord(value)) {
19554
19588
  return value;
19555
19589
  }
19590
+ const nextWithinFileField = withinFileField || this.isFileFieldRecord(value);
19556
19591
  const sanitized = {};
19557
19592
  for (const [key, entryValue] of Object.entries(value)) {
19558
- if (key === 'data' && this.shouldHideFileData(value, entryValue)) {
19593
+ if (key === 'data' && this.shouldHideFileData(nextWithinFileField, entryValue)) {
19559
19594
  continue;
19560
19595
  }
19561
- sanitized[key] = this.sanitizePreviewData(entryValue);
19596
+ sanitized[key] = this.sanitizePreviewData(entryValue, nextWithinFileField);
19562
19597
  }
19563
19598
  return sanitized;
19564
19599
  }
19565
19600
  isObjectRecord(value) {
19566
19601
  return !!value && typeof value === 'object';
19567
19602
  }
19568
- shouldHideFileData(record, value) {
19569
- if (!this.isFileFieldRecord(record))
19603
+ shouldHideFileData(withinFileField, value) {
19604
+ if (!withinFileField)
19570
19605
  return false;
19571
19606
  return this.isBytePayload(value) || this.isBase64Payload(value);
19572
19607
  }
19573
19608
  isFileFieldRecord(value) {
19574
19609
  return typeof value['fieldName'] === 'string'
19575
19610
  && Object.hasOwn(value, 'fieldValue')
19576
- && Object.hasOwn(value, 'fileType');
19611
+ && (value['fieldType'] === 'file' || Object.hasOwn(value, 'fileType'));
19577
19612
  }
19578
19613
  isBytePayload(value) {
19579
19614
  if (value instanceof Uint8Array)
@@ -31774,7 +31809,20 @@ function compareSortableValues(left, right) {
31774
31809
  }
31775
31810
  return String(left ?? '').localeCompare(String(right ?? ''));
31776
31811
  }
31812
+ function toDisplayText(value) {
31813
+ if (typeof value === 'string') {
31814
+ const trimmed = value.trim();
31815
+ return trimmed.length > 0 ? trimmed : undefined;
31816
+ }
31817
+ if (typeof value === 'number' || typeof value === 'boolean') {
31818
+ return String(value);
31819
+ }
31820
+ return undefined;
31821
+ }
31777
31822
  class DataProvider {
31823
+ async getValueDisplayPrefix(_field, _engine) {
31824
+ return '';
31825
+ }
31778
31826
  // Non-abstract with default implementation for backward compatibility
31779
31827
  async queryOptions(field, query, engine) {
31780
31828
  const options = await this.getOptions(field, engine);
@@ -31952,17 +32000,11 @@ class DefaultDataProvider extends DataProvider {
31952
32000
  return cfg.staticValue;
31953
32001
  return currentValue !== undefined ? currentValue : field.defaultValue;
31954
32002
  }
31955
- let rows = await this.getRawRows(cfg, field, engine);
31956
- rows = this.resolveCollectionRows(rows, cfg, engine);
31957
- rows = this.applyRowFilters(rows, cfg.filters, engine);
31958
- if (!rows || rows.length === 0) {
31959
- return currentValue !== undefined ? currentValue : field.defaultValue;
31960
- }
31961
- const selectedRow = this.selectScalarRow(rows, cfg, engine);
31962
- if (cfg.rowSelectionMode === 'selected' && !selectedRow) {
32003
+ const context = await this.getScalarValueContext(cfg, field, engine);
32004
+ if (!context) {
31963
32005
  return currentValue !== undefined ? currentValue : field.defaultValue;
31964
32006
  }
31965
- const row = selectedRow ?? rows[0];
32007
+ const row = context.row;
31966
32008
  const resolvedPath = cfg.valueKey;
31967
32009
  const resolvedValue = resolvePathValue(row, resolvedPath);
31968
32010
  if (resolvedPath && resolvedValue !== undefined) {
@@ -31973,6 +32015,27 @@ class DefaultDataProvider extends DataProvider {
31973
32015
  }
31974
32016
  return row;
31975
32017
  }
32018
+ async getValueDisplayPrefix(field, engine) {
32019
+ const cfg = getEffectiveDataConfig(field);
32020
+ const prefixPath = cfg.optionLabelPrefixPath?.trim();
32021
+ if (!prefixPath) {
32022
+ return '';
32023
+ }
32024
+ const context = await this.getScalarValueContext(cfg, field, engine);
32025
+ if (!context) {
32026
+ return '';
32027
+ }
32028
+ for (const candidate of [context.row, context.parentRow, context.sourceRow]) {
32029
+ if (!candidate)
32030
+ continue;
32031
+ const resolved = resolvePathValue(candidate, prefixPath);
32032
+ const value = toDisplayText(resolved);
32033
+ if (value) {
32034
+ return value;
32035
+ }
32036
+ }
32037
+ return '';
32038
+ }
31976
32039
  async resolveValue(field, value) {
31977
32040
  return String(value);
31978
32041
  }
@@ -32020,6 +32083,18 @@ class DefaultDataProvider extends DataProvider {
32020
32083
  const rows = await this.getRawRows(cfg, field, engine);
32021
32084
  return this.resolveCollectionRowContexts(rows, cfg, engine);
32022
32085
  }
32086
+ async getScalarValueContext(cfg, field, engine) {
32087
+ const contexts = await this.getOptionRowContexts(cfg, field, engine);
32088
+ const filteredContexts = this.applyOptionRowFilters(contexts, cfg.filters, engine);
32089
+ if (filteredContexts.length === 0) {
32090
+ return undefined;
32091
+ }
32092
+ const selectedContext = this.selectOptionContext(filteredContexts, cfg, engine);
32093
+ if (cfg.rowSelectionMode === 'selected' && !selectedContext) {
32094
+ return undefined;
32095
+ }
32096
+ return selectedContext ?? filteredContexts[0];
32097
+ }
32023
32098
  resolveCollectionRowContexts(rows, cfg, engine) {
32024
32099
  const parentContexts = this.extractRowContexts(rows, cfg.rowsPath);
32025
32100
  if (cfg.rowSelectionMode === 'selected' && cfg.childRowsPath) {
@@ -32240,10 +32315,8 @@ class DefaultDataProvider extends DataProvider {
32240
32315
  if (!candidate)
32241
32316
  continue;
32242
32317
  const resolved = resolvePathValue(candidate, prefixPath);
32243
- if (resolved === undefined || resolved === null)
32244
- continue;
32245
- const value = String(resolved).trim();
32246
- if (value.length > 0) {
32318
+ const value = toDisplayText(resolved);
32319
+ if (value) {
32247
32320
  return value;
32248
32321
  }
32249
32322
  }
@@ -32436,6 +32509,8 @@ class TextFieldWidgetComponent {
32436
32509
  dependencyValueSnapshot = new Map();
32437
32510
  isControlFocused = false;
32438
32511
  isControlHovered = false;
32512
+ displayPrefix = '';
32513
+ formattedNumberValue = '';
32439
32514
  set config(value) {
32440
32515
  const previousSignature = this.getDataConfigSignature(this._config);
32441
32516
  this._config = value;
@@ -32450,7 +32525,10 @@ class TextFieldWidgetComponent {
32450
32525
  if (this.dataProvider && this._config?.dataConfig) {
32451
32526
  this.seedDependencySnapshotFromEngine();
32452
32527
  void this.refreshValueFromDataSource();
32528
+ return;
32453
32529
  }
32530
+ this.displayPrefix = '';
32531
+ this.syncFormattedNumberValue();
32454
32532
  }
32455
32533
  get config() {
32456
32534
  return this._config;
@@ -32473,6 +32551,12 @@ class TextFieldWidgetComponent {
32473
32551
  isColorField() {
32474
32552
  return this.config?.type === 'color';
32475
32553
  }
32554
+ usesFormattedNumberInput() {
32555
+ return this.config?.type === 'number' && this.config?.dataConfig?.formatNumericOptionLabels === true;
32556
+ }
32557
+ hasDisplayPrefix() {
32558
+ return !this.isTextarea() && !this.isColorField() && this.displayPrefix.trim().length > 0;
32559
+ }
32476
32560
  getControlClass(multiline = false) {
32477
32561
  return getTextControlClass({
32478
32562
  invalid: !!this.error,
@@ -32510,6 +32594,9 @@ class TextFieldWidgetComponent {
32510
32594
  if (this.dataProvider && this.config.dataConfig) {
32511
32595
  void this.refreshValueFromDataSource();
32512
32596
  }
32597
+ else {
32598
+ this.syncFormattedNumberValue();
32599
+ }
32513
32600
  // Sync enabled state based on config + rules
32514
32601
  this.syncEnabledState();
32515
32602
  if (this.engine) {
@@ -32545,6 +32632,9 @@ class TextFieldWidgetComponent {
32545
32632
  this.control.valueChanges
32546
32633
  .pipe(takeUntilDestroyed(this.destroyRef))
32547
32634
  .subscribe(val => {
32635
+ if (this.usesFormattedNumberInput() && !this.isControlFocused) {
32636
+ this.syncFormattedNumberValue();
32637
+ }
32548
32638
  if (this.engine) {
32549
32639
  this.engine.setValue(this.config.name, val);
32550
32640
  this.engine.emitUiEvent({ fieldId: this.config.id, fieldName: this.config.name, type: 'change', value: val });
@@ -32556,6 +32646,7 @@ class TextFieldWidgetComponent {
32556
32646
  }
32557
32647
  onFocus() {
32558
32648
  this.isControlFocused = true;
32649
+ this.syncFormattedNumberValue();
32559
32650
  if (this.engine)
32560
32651
  this.engine.emitUiEvent({ fieldId: this.config.id, fieldName: this.config.name, type: 'focus' });
32561
32652
  }
@@ -32565,6 +32656,8 @@ class TextFieldWidgetComponent {
32565
32656
  }
32566
32657
  onBlur() {
32567
32658
  this.isControlFocused = false;
32659
+ this.control.markAsTouched();
32660
+ this.syncFormattedNumberValue();
32568
32661
  if (this.engine) {
32569
32662
  this.engine.emitUiEvent({ fieldId: this.config.id, fieldName: this.config.name, type: 'blur' });
32570
32663
  if (this.control.touched) {
@@ -32612,6 +32705,15 @@ class TextFieldWidgetComponent {
32612
32705
  invalid: !!this.error
32613
32706
  }), splitControlSurfaceStyles(this.config.style).controlStyles);
32614
32707
  }
32708
+ getSingleLineControlStyles() {
32709
+ const styles = this.getControlStyles();
32710
+ if (!this.hasDisplayPrefix()) {
32711
+ return styles;
32712
+ }
32713
+ return mergeAndNormalize(styles, {
32714
+ paddingLeft: `calc(20px + ${Math.max(this.displayPrefix.length, 1)}ch)`
32715
+ });
32716
+ }
32615
32717
  hasWrapperFrame() {
32616
32718
  return hasWrapperSurfaceStyles(this.config.style);
32617
32719
  }
@@ -32650,9 +32752,11 @@ class TextFieldWidgetComponent {
32650
32752
  return;
32651
32753
  try {
32652
32754
  const val = await this.dataProvider.getValue(this.config, this.engine);
32755
+ this.displayPrefix = await this.dataProvider.getValueDisplayPrefix(this.config, this.engine);
32653
32756
  if (val === undefined || val === null)
32654
32757
  return;
32655
32758
  this.control.setValue(val, { emitEvent: false });
32759
+ this.syncFormattedNumberValue();
32656
32760
  if (this.engine) {
32657
32761
  this.engine.setValue(this.config.name, val);
32658
32762
  }
@@ -32678,6 +32782,9 @@ class TextFieldWidgetComponent {
32678
32782
  sourceKey,
32679
32783
  String(dataConfig.valueKey ?? ''),
32680
32784
  String(dataConfig.rowsPath ?? ''),
32785
+ String(dataConfig.formatNumericOptionLabels ?? ''),
32786
+ String(dataConfig.optionLabelPrefixPath ?? ''),
32787
+ String(dataConfig.optionLabelPrefixFieldId ?? ''),
32681
32788
  String(dataConfig.rowSelectionMode ?? ''),
32682
32789
  String(dataConfig.selectionFieldId ?? ''),
32683
32790
  String(dataConfig.selectionMatchPath ?? ''),
@@ -32733,6 +32840,59 @@ class TextFieldWidgetComponent {
32733
32840
  .filter(field => dependencyIds.has(field.id))
32734
32841
  .map(field => field.name);
32735
32842
  }
32843
+ onFormattedNumberInput(event) {
32844
+ const input = event.target;
32845
+ this.formattedNumberValue = input.value;
32846
+ if (this.control.pristine) {
32847
+ this.control.markAsDirty();
32848
+ }
32849
+ const normalized = input.value.replace(/,/g, '').trim();
32850
+ if (!normalized) {
32851
+ this.control.setValue(null);
32852
+ return;
32853
+ }
32854
+ const parsed = Number(normalized);
32855
+ this.control.setValue(Number.isFinite(parsed) ? parsed : null);
32856
+ }
32857
+ syncFormattedNumberValue() {
32858
+ if (!this.usesFormattedNumberInput()) {
32859
+ return;
32860
+ }
32861
+ const rawValue = this.control.value;
32862
+ if (rawValue === undefined || rawValue === null || rawValue === '') {
32863
+ this.formattedNumberValue = '';
32864
+ return;
32865
+ }
32866
+ if (typeof rawValue !== 'string' && typeof rawValue !== 'number') {
32867
+ this.formattedNumberValue = '';
32868
+ return;
32869
+ }
32870
+ const normalized = String(rawValue).replace(/,/g, '').trim();
32871
+ if (!normalized) {
32872
+ this.formattedNumberValue = '';
32873
+ return;
32874
+ }
32875
+ if (this.isControlFocused) {
32876
+ this.formattedNumberValue = normalized;
32877
+ return;
32878
+ }
32879
+ this.formattedNumberValue = this.formatNumericValue(normalized);
32880
+ }
32881
+ formatNumericValue(value) {
32882
+ if (!/^-?\d+(\.\d+)?$/.test(value)) {
32883
+ return value;
32884
+ }
32885
+ const numericValue = Number(value);
32886
+ if (!Number.isFinite(numericValue)) {
32887
+ return value;
32888
+ }
32889
+ const fractionPart = value.split('.')[1];
32890
+ return new Intl.NumberFormat(undefined, {
32891
+ useGrouping: true,
32892
+ minimumFractionDigits: fractionPart?.length ?? 0,
32893
+ maximumFractionDigits: fractionPart?.length ?? 0
32894
+ }).format(numericValue);
32895
+ }
32736
32896
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: TextFieldWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
32737
32897
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.17", type: TextFieldWidgetComponent, isStandalone: true, selector: "app-text-field-widget", inputs: { config: "config", engine: "engine", control: "control" }, ngImport: i0, template: `
32738
32898
  <div [class]="fieldContainerClass"
@@ -32797,7 +32957,46 @@ class TextFieldWidgetComponent {
32797
32957
  [cpFallbackColor]="'#000000'"
32798
32958
  [cpDisabled]="config.readonly || !enabled"
32799
32959
  (colorPickerChange)="onColorPickerChange($event)">
32960
+ } @else if (usesFormattedNumberInput()) {
32961
+ @if (hasDisplayPrefix()) {
32962
+ <span
32963
+ class="pointer-events-none absolute left-3 top-1/2 z-[1] -translate-y-1/2 text-sm text-slate-500"
32964
+ data-fd-field-prefix>
32965
+ {{ displayPrefix }}
32966
+ </span>
32967
+ }
32968
+ <input
32969
+ [id]="fieldId"
32970
+ type="text"
32971
+ inputmode="decimal"
32972
+ [placeholder]="config.placeholder || ''"
32973
+ [value]="formattedNumberValue"
32974
+ (input)="onFormattedNumberInput($event)"
32975
+ (click)="onClick()"
32976
+ (focus)="onFocus()"
32977
+ (blur)="onBlur()"
32978
+ (mouseenter)="onMouseEnter()"
32979
+ (mouseleave)="onMouseLeave()"
32980
+ [readonly]="config.readonly"
32981
+ [disabled]="control.disabled"
32982
+ [attr.aria-required]="required"
32983
+ [attr.aria-invalid]="!!error"
32984
+ [attr.aria-describedby]="getAriaDescribedBy()"
32985
+ [attr.aria-label]="getAccessibleLabel()"
32986
+ [class]="getControlClass()"
32987
+ [ngStyle]="getSingleLineControlStyles()"
32988
+ data-fd="field-control"
32989
+ [attr.min]="config.html5?.min"
32990
+ [attr.max]="config.html5?.max"
32991
+ [attr.step]="config.html5?.step">
32800
32992
  } @else {
32993
+ @if (hasDisplayPrefix()) {
32994
+ <span
32995
+ class="pointer-events-none absolute left-3 top-1/2 z-[1] -translate-y-1/2 text-sm text-slate-500"
32996
+ data-fd-field-prefix>
32997
+ {{ displayPrefix }}
32998
+ </span>
32999
+ }
32801
33000
  <input
32802
33001
  [id]="fieldId"
32803
33002
  [type]="config.type"
@@ -32814,7 +33013,7 @@ class TextFieldWidgetComponent {
32814
33013
  [attr.aria-describedby]="getAriaDescribedBy()"
32815
33014
  [attr.aria-label]="getAccessibleLabel()"
32816
33015
  [class]="getControlClass()"
32817
- [ngStyle]="getControlStyles()"
33016
+ [ngStyle]="getSingleLineControlStyles()"
32818
33017
  data-fd="field-control"
32819
33018
  [attr.min]="config.html5?.min"
32820
33019
  [attr.max]="config.html5?.max"
@@ -32901,7 +33100,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
32901
33100
  [cpFallbackColor]="'#000000'"
32902
33101
  [cpDisabled]="config.readonly || !enabled"
32903
33102
  (colorPickerChange)="onColorPickerChange($event)">
33103
+ } @else if (usesFormattedNumberInput()) {
33104
+ @if (hasDisplayPrefix()) {
33105
+ <span
33106
+ class="pointer-events-none absolute left-3 top-1/2 z-[1] -translate-y-1/2 text-sm text-slate-500"
33107
+ data-fd-field-prefix>
33108
+ {{ displayPrefix }}
33109
+ </span>
33110
+ }
33111
+ <input
33112
+ [id]="fieldId"
33113
+ type="text"
33114
+ inputmode="decimal"
33115
+ [placeholder]="config.placeholder || ''"
33116
+ [value]="formattedNumberValue"
33117
+ (input)="onFormattedNumberInput($event)"
33118
+ (click)="onClick()"
33119
+ (focus)="onFocus()"
33120
+ (blur)="onBlur()"
33121
+ (mouseenter)="onMouseEnter()"
33122
+ (mouseleave)="onMouseLeave()"
33123
+ [readonly]="config.readonly"
33124
+ [disabled]="control.disabled"
33125
+ [attr.aria-required]="required"
33126
+ [attr.aria-invalid]="!!error"
33127
+ [attr.aria-describedby]="getAriaDescribedBy()"
33128
+ [attr.aria-label]="getAccessibleLabel()"
33129
+ [class]="getControlClass()"
33130
+ [ngStyle]="getSingleLineControlStyles()"
33131
+ data-fd="field-control"
33132
+ [attr.min]="config.html5?.min"
33133
+ [attr.max]="config.html5?.max"
33134
+ [attr.step]="config.html5?.step">
32904
33135
  } @else {
33136
+ @if (hasDisplayPrefix()) {
33137
+ <span
33138
+ class="pointer-events-none absolute left-3 top-1/2 z-[1] -translate-y-1/2 text-sm text-slate-500"
33139
+ data-fd-field-prefix>
33140
+ {{ displayPrefix }}
33141
+ </span>
33142
+ }
32905
33143
  <input
32906
33144
  [id]="fieldId"
32907
33145
  [type]="config.type"
@@ -32918,7 +33156,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
32918
33156
  [attr.aria-describedby]="getAriaDescribedBy()"
32919
33157
  [attr.aria-label]="getAccessibleLabel()"
32920
33158
  [class]="getControlClass()"
32921
- [ngStyle]="getControlStyles()"
33159
+ [ngStyle]="getSingleLineControlStyles()"
32922
33160
  data-fd="field-control"
32923
33161
  [attr.min]="config.html5?.min"
32924
33162
  [attr.max]="config.html5?.max"
@@ -37660,6 +37898,10 @@ function generateId$2() {
37660
37898
  const COMMON_GROUP_FIELDS = [
37661
37899
  { key: 'groupKey', type: 'text', label: 'Group Key', placeholder: 'documents', helpText: 'Group fields into one object on submit/preview (uses the field key inside the group).' }
37662
37900
  ];
37901
+ const COMMON_INTERACTIVITY_FIELDS = [
37902
+ { key: 'readonly', type: 'checkbox', label: 'Read Only' },
37903
+ { key: 'disabled', type: 'checkbox', label: 'Disabled' }
37904
+ ];
37663
37905
  const COMMON_BASIC_FIELDS = [
37664
37906
  { key: 'name', type: 'text', label: 'Field Key (Name)' },
37665
37907
  { key: 'label', type: 'text', label: 'Label' },
@@ -37667,6 +37909,7 @@ const COMMON_BASIC_FIELDS = [
37667
37909
  { key: 'placeholder', type: 'text', label: 'Placeholder' },
37668
37910
  { key: 'helpText', type: 'text', label: 'Help Text' }, // Hint
37669
37911
  { key: 'tooltip', type: 'text', label: 'Tooltip' },
37912
+ ...COMMON_INTERACTIVITY_FIELDS,
37670
37913
  ...COMMON_GROUP_FIELDS
37671
37914
  ];
37672
37915
  const FILE_BASIC_FIELDS = [
@@ -37675,6 +37918,7 @@ const FILE_BASIC_FIELDS = [
37675
37918
  { key: 'showLabel', type: 'checkbox', label: 'Show Native Label' },
37676
37919
  { key: 'helpText', type: 'text', label: 'Help Text' },
37677
37920
  { key: 'tooltip', type: 'text', label: 'Tooltip' },
37921
+ ...COMMON_INTERACTIVITY_FIELDS,
37678
37922
  ...COMMON_GROUP_FIELDS
37679
37923
  ];
37680
37924
  const COMMON_APPEARANCE_FIELDS = [
@@ -37699,9 +37943,7 @@ const COMMON_APPEARANCE_FIELDS = [
37699
37943
  { label: 'Third Width (33%)', value: '33.33%' },
37700
37944
  { label: 'Quarter Width (25%)', value: '25%' }
37701
37945
  ]
37702
- },
37703
- { key: 'readonly', type: 'checkbox', label: 'Read Only' },
37704
- { key: 'disabled', type: 'checkbox', label: 'Disabled' }
37946
+ }
37705
37947
  ];
37706
37948
  const COMMON_STYLE_SECTIONS = [
37707
37949
  STYLE_LAYOUT_SECTION,
@@ -37752,6 +37994,7 @@ const BUTTON_PROPERTIES = [
37752
37994
  label: 'Style',
37753
37995
  fields: [
37754
37996
  { key: 'variant', type: 'select', label: 'Variant', options: BUTTON_VARIANT_OPTIONS },
37997
+ ...COMMON_INTERACTIVITY_FIELDS,
37755
37998
  ...COMMON_APPEARANCE_FIELDS
37756
37999
  ]
37757
38000
  },
@@ -37770,6 +38013,7 @@ const IMAGE_BUTTON_PROPERTIES = [
37770
38013
  label: 'Style',
37771
38014
  fields: [
37772
38015
  { key: 'variant', type: 'select', label: 'Variant', options: BUTTON_VARIANT_OPTIONS },
38016
+ ...COMMON_INTERACTIVITY_FIELDS,
37773
38017
  ...COMMON_APPEARANCE_FIELDS
37774
38018
  ]
37775
38019
  },
@@ -37819,6 +38063,7 @@ const REPEATABLE_WIDGET_PROPERTIES = [
37819
38063
  { key: 'showLabel', type: 'checkbox', label: 'Show Native Label' },
37820
38064
  { key: 'helpText', type: 'text', label: 'Help Text' },
37821
38065
  { key: 'tooltip', type: 'text', label: 'Tooltip' },
38066
+ ...COMMON_INTERACTIVITY_FIELDS,
37822
38067
  ...COMMON_GROUP_FIELDS
37823
38068
  ]
37824
38069
  },