@bpmn-io/form-js-viewer 1.1.0 → 1.3.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.
Files changed (32) hide show
  1. package/dist/assets/form-js-base.css +54 -1
  2. package/dist/assets/form-js.css +54 -1
  3. package/dist/index.cjs +1074 -636
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.es.js +1070 -636
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/types/Form.d.ts +4 -0
  8. package/dist/types/core/FieldFactory.d.ts +19 -0
  9. package/dist/types/core/FormFieldRegistry.d.ts +0 -1
  10. package/dist/types/core/Importer.d.ts +56 -0
  11. package/dist/types/core/PathRegistry.d.ts +71 -0
  12. package/dist/types/core/index.d.ts +9 -5
  13. package/dist/types/features/expression-language/ConditionChecker.d.ts +3 -2
  14. package/dist/types/index.d.ts +2 -2
  15. package/dist/types/render/components/form-fields/Checklist.d.ts +2 -6
  16. package/dist/types/render/components/form-fields/Default.d.ts +3 -3
  17. package/dist/types/render/components/form-fields/Group.d.ts +14 -0
  18. package/dist/types/render/components/form-fields/Radio.d.ts +2 -6
  19. package/dist/types/render/components/form-fields/Select.d.ts +2 -6
  20. package/dist/types/render/components/form-fields/Taglist.d.ts +2 -6
  21. package/dist/types/render/components/form-fields/parts/Grid.d.ts +1 -0
  22. package/dist/types/render/components/index.d.ts +4 -2
  23. package/dist/types/render/components/util/localisationUtil.d.ts +24 -0
  24. package/dist/types/render/components/util/valuesUtil.d.ts +6 -0
  25. package/dist/types/render/context/FormRenderContext.d.ts +3 -0
  26. package/dist/types/render/hooks/index.d.ts +2 -0
  27. package/dist/types/render/hooks/useDeepCompareState.d.ts +8 -0
  28. package/dist/types/render/hooks/usePrevious.d.ts +1 -0
  29. package/dist/types/util/index.d.ts +1 -2
  30. package/package.json +3 -2
  31. package/dist/types/import/Importer.d.ts +0 -45
  32. package/dist/types/import/index.d.ts +0 -5
package/dist/index.cjs CHANGED
@@ -407,14 +407,230 @@ class FeelersTemplating {
407
407
  }
408
408
  FeelersTemplating.$inject = [];
409
409
 
410
+ // config ///////////////////
411
+
412
+ const MINUTES_IN_DAY = 60 * 24;
413
+ const DATETIME_SUBTYPES = {
414
+ DATE: 'date',
415
+ TIME: 'time',
416
+ DATETIME: 'datetime'
417
+ };
418
+ const TIME_SERIALISING_FORMATS = {
419
+ UTC_OFFSET: 'utc_offset',
420
+ UTC_NORMALIZED: 'utc_normalized',
421
+ NO_TIMEZONE: 'no_timezone'
422
+ };
423
+ const DATETIME_SUBTYPES_LABELS = {
424
+ [DATETIME_SUBTYPES.DATE]: 'Date',
425
+ [DATETIME_SUBTYPES.TIME]: 'Time',
426
+ [DATETIME_SUBTYPES.DATETIME]: 'Date & Time'
427
+ };
428
+ const TIME_SERIALISINGFORMAT_LABELS = {
429
+ [TIME_SERIALISING_FORMATS.UTC_OFFSET]: 'UTC offset',
430
+ [TIME_SERIALISING_FORMATS.UTC_NORMALIZED]: 'UTC normalized',
431
+ [TIME_SERIALISING_FORMATS.NO_TIMEZONE]: 'No timezone'
432
+ };
433
+ const DATETIME_SUBTYPE_PATH = ['subtype'];
434
+ const DATE_LABEL_PATH = ['dateLabel'];
435
+ const DATE_DISALLOW_PAST_PATH = ['disallowPassedDates'];
436
+ const TIME_LABEL_PATH = ['timeLabel'];
437
+ const TIME_USE24H_PATH = ['use24h'];
438
+ const TIME_INTERVAL_PATH = ['timeInterval'];
439
+ const TIME_SERIALISING_FORMAT_PATH = ['timeSerializingFormat'];
440
+
441
+ // config ///////////////////
442
+
443
+ const VALUES_SOURCES = {
444
+ STATIC: 'static',
445
+ INPUT: 'input',
446
+ EXPRESSION: 'expression'
447
+ };
448
+ const VALUES_SOURCE_DEFAULT = VALUES_SOURCES.STATIC;
449
+ const VALUES_SOURCES_LABELS = {
450
+ [VALUES_SOURCES.STATIC]: 'Static',
451
+ [VALUES_SOURCES.INPUT]: 'Input data',
452
+ [VALUES_SOURCES.EXPRESSION]: 'Expression'
453
+ };
454
+ const VALUES_SOURCES_PATHS = {
455
+ [VALUES_SOURCES.STATIC]: ['values'],
456
+ [VALUES_SOURCES.INPUT]: ['valuesKey'],
457
+ [VALUES_SOURCES.EXPRESSION]: ['valuesExpression']
458
+ };
459
+ const VALUES_SOURCES_DEFAULTS = {
460
+ [VALUES_SOURCES.STATIC]: [{
461
+ label: 'Value',
462
+ value: 'value'
463
+ }],
464
+ [VALUES_SOURCES.INPUT]: '',
465
+ [VALUES_SOURCES.EXPRESSION]: '='
466
+ };
467
+
468
+ // helpers ///////////////////
469
+
470
+ function getValuesSource(field) {
471
+ for (const source of Object.values(VALUES_SOURCES)) {
472
+ if (minDash.get(field, VALUES_SOURCES_PATHS[source]) !== undefined) {
473
+ return source;
474
+ }
475
+ }
476
+ return VALUES_SOURCE_DEFAULT;
477
+ }
478
+
479
+ function createInjector(bootstrapModules) {
480
+ const injector = new didi.Injector(bootstrapModules);
481
+ injector.init();
482
+ return injector;
483
+ }
484
+
485
+ /**
486
+ * @param {string?} prefix
487
+ *
488
+ * @returns Element
489
+ */
490
+ function createFormContainer(prefix = 'fjs') {
491
+ const container = document.createElement('div');
492
+ container.classList.add(`${prefix}-container`);
493
+ return container;
494
+ }
495
+
496
+ const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression'];
497
+ const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text'];
498
+ function isRequired(field) {
499
+ return field.required;
500
+ }
501
+ function pathParse(path) {
502
+ if (!path) {
503
+ return [];
504
+ }
505
+ return path.split('.').map(key => {
506
+ return isNaN(parseInt(key)) ? key : parseInt(key);
507
+ });
508
+ }
509
+ function pathsEqual(a, b) {
510
+ return a && b && a.length === b.length && a.every((value, index) => value === b[index]);
511
+ }
512
+ const indices = {};
513
+ function generateIndexForType(type) {
514
+ if (type in indices) {
515
+ indices[type]++;
516
+ } else {
517
+ indices[type] = 1;
518
+ }
519
+ return indices[type];
520
+ }
521
+ function generateIdForType(type) {
522
+ return `${type}${generateIndexForType(type)}`;
523
+ }
524
+
525
+ /**
526
+ * @template T
527
+ * @param {T} data
528
+ * @param {(this: any, key: string, value: any) => any} [replacer]
529
+ * @return {T}
530
+ */
531
+ function clone(data, replacer) {
532
+ return JSON.parse(JSON.stringify(data, replacer));
533
+ }
534
+
535
+ /**
536
+ * Parse the schema for input variables a form might make use of
537
+ *
538
+ * @param {any} schema
539
+ *
540
+ * @return {string[]}
541
+ */
542
+ function getSchemaVariables(schema, options = {}) {
543
+ const {
544
+ expressionLanguage = new FeelExpressionLanguage(null),
545
+ templating = new FeelersTemplating(),
546
+ inputs = true,
547
+ outputs = true
548
+ } = options;
549
+ if (!schema.components) {
550
+ return [];
551
+ }
552
+ const getAllComponents = node => {
553
+ const components = [];
554
+ if (node.components) {
555
+ node.components.forEach(component => {
556
+ components.push(component);
557
+ components.push(...getAllComponents(component));
558
+ });
559
+ }
560
+ return components;
561
+ };
562
+ const variables = getAllComponents(schema).reduce((variables, component) => {
563
+ const {
564
+ valuesKey
565
+ } = component;
566
+
567
+ // collect input-only variables
568
+ if (inputs) {
569
+ if (valuesKey) {
570
+ variables = [...variables, valuesKey];
571
+ }
572
+ EXPRESSION_PROPERTIES.forEach(prop => {
573
+ const property = minDash.get(component, prop.split('.'));
574
+ if (property && expressionLanguage.isExpression(property)) {
575
+ const expressionVariables = expressionLanguage.getVariableNames(property, {
576
+ type: 'expression'
577
+ });
578
+ variables = [...variables, ...expressionVariables];
579
+ }
580
+ });
581
+ TEMPLATE_PROPERTIES.forEach(prop => {
582
+ const property = minDash.get(component, prop.split('.'));
583
+ if (property && !expressionLanguage.isExpression(property) && templating.isTemplate(property)) {
584
+ const templateVariables = templating.getVariableNames(property);
585
+ variables = [...variables, ...templateVariables];
586
+ }
587
+ });
588
+ }
589
+ return variables.filter(variable => variable !== undefined || variable !== null);
590
+ }, []);
591
+ const getBindingVariables = node => {
592
+ const bindingVariable = [];
593
+
594
+ // c.f. https://github.com/bpmn-io/form-js/issues/778 @Skaiir to remove?
595
+ if (node.type === 'button') {
596
+ return [];
597
+ } else if (node.key) {
598
+ return [node.key.split('.')[0]];
599
+ } else if (node.path) {
600
+ return [node.path.split('.')[0]];
601
+ } else if (node.components) {
602
+ node.components.forEach(component => {
603
+ bindingVariable.push(...getBindingVariables(component));
604
+ });
605
+ }
606
+ return bindingVariable;
607
+ };
608
+
609
+ // collect binding variables
610
+ if (inputs || outputs) {
611
+ variables.push(...getBindingVariables(schema));
612
+ }
613
+
614
+ // remove duplicates
615
+ return Array.from(new Set(variables));
616
+ }
617
+ function runRecursively(formField, fn) {
618
+ const components = formField.components || [];
619
+ components.forEach((component, index) => {
620
+ runRecursively(component, fn);
621
+ });
622
+ fn(formField);
623
+ }
624
+
410
625
  /**
411
626
  * @typedef {object} Condition
412
627
  * @property {string} [hide]
413
628
  */
414
629
 
415
630
  class ConditionChecker {
416
- constructor(formFieldRegistry, eventBus) {
631
+ constructor(formFieldRegistry, pathRegistry, eventBus) {
417
632
  this._formFieldRegistry = formFieldRegistry;
633
+ this._pathRegistry = pathRegistry;
418
634
  this._eventBus = eventBus;
419
635
  }
420
636
 
@@ -425,19 +641,27 @@ class ConditionChecker {
425
641
  * @param {Object<string, any>} data
426
642
  */
427
643
  applyConditions(properties, data = {}) {
428
- const conditions = this._getConditions();
429
- const newProperties = {
430
- ...properties
431
- };
432
- for (const {
433
- key,
434
- condition
435
- } of conditions) {
436
- const shouldRemove = this._checkHideCondition(condition, data);
437
- if (shouldRemove) {
438
- delete newProperties[key];
439
- }
644
+ const newProperties = clone(properties);
645
+ const form = this._formFieldRegistry.getAll().find(field => field.type === 'default');
646
+ if (!form) {
647
+ throw new Error('form field registry has no form');
440
648
  }
649
+ this._pathRegistry.executeRecursivelyOnFields(form, ({
650
+ field,
651
+ isClosed,
652
+ context
653
+ }) => {
654
+ const {
655
+ conditional: condition
656
+ } = field;
657
+ context.isHidden = context.isHidden || condition && this._checkHideCondition(condition, data);
658
+
659
+ // only clear the leaf nodes, as groups may both point to the same path
660
+ if (context.isHidden && isClosed) {
661
+ const valuePath = this._pathRegistry.getValuePath(field);
662
+ this._clearObjectValueRecursively(valuePath, newProperties);
663
+ }
664
+ });
441
665
  return newProperties;
442
666
  }
443
667
 
@@ -482,24 +706,18 @@ class ConditionChecker {
482
706
  const result = this.check(condition.hide, data);
483
707
  return result === true;
484
708
  }
485
- _getConditions() {
486
- const formFields = this._formFieldRegistry.getAll();
487
- return formFields.reduce((conditions, formField) => {
488
- const {
489
- key,
490
- conditional: condition
491
- } = formField;
492
- if (key && condition) {
493
- return [...conditions, {
494
- key,
495
- condition
496
- }];
497
- }
498
- return conditions;
499
- }, []);
709
+ _clearObjectValueRecursively(valuePath, obj) {
710
+ const workingValuePath = [...valuePath];
711
+ let recurse = false;
712
+ do {
713
+ minDash.set(obj, workingValuePath, undefined);
714
+ workingValuePath.pop();
715
+ const parentObject = minDash.get(obj, workingValuePath);
716
+ recurse = minDash.isObject(parentObject) && !minDash.values(parentObject).length && !!workingValuePath.length;
717
+ } while (recurse);
500
718
  }
501
719
  }
502
- ConditionChecker.$inject = ['formFieldRegistry', 'eventBus'];
720
+ ConditionChecker.$inject = ['formFieldRegistry', 'pathRegistry', 'eventBus'];
503
721
 
504
722
  var ExpressionLanguageModule = {
505
723
  __init__: ['expressionLanguage', 'templating', 'conditionChecker'],
@@ -967,239 +1185,42 @@ CommandStack.prototype._popAction = function () {
967
1185
  execution.trigger = null;
968
1186
  }
969
1187
  };
970
- CommandStack.prototype._markDirty = function (elements) {
971
- const execution = this._currentExecution;
972
- if (!elements) {
973
- return;
974
- }
975
- elements = minDash.isArray(elements) ? elements : [elements];
976
- execution.dirty = execution.dirty.concat(elements);
977
- };
978
- CommandStack.prototype._executedAction = function (action, redo) {
979
- const stackIdx = ++this._stackIdx;
980
- if (!redo) {
981
- this._stack.splice(stackIdx, this._stack.length, action);
982
- }
983
- };
984
- CommandStack.prototype._revertedAction = function (action) {
985
- this._stackIdx--;
986
- };
987
- CommandStack.prototype._getHandler = function (command) {
988
- return this._handlerMap[command];
989
- };
990
- CommandStack.prototype._setHandler = function (command, handler) {
991
- if (!command || !handler) {
992
- throw new Error('command and handler required');
993
- }
994
- if (this._handlerMap[command]) {
995
- throw new Error('overriding handler for command <' + command + '>');
996
- }
997
- this._handlerMap[command] = handler;
998
- };
999
-
1000
- /**
1001
- * @type { import('didi').ModuleDeclaration }
1002
- */
1003
- var commandModule = {
1004
- commandStack: ['type', CommandStack]
1005
- };
1006
-
1007
- // config ///////////////////
1008
-
1009
- const MINUTES_IN_DAY = 60 * 24;
1010
- const DATETIME_SUBTYPES = {
1011
- DATE: 'date',
1012
- TIME: 'time',
1013
- DATETIME: 'datetime'
1014
- };
1015
- const TIME_SERIALISING_FORMATS = {
1016
- UTC_OFFSET: 'utc_offset',
1017
- UTC_NORMALIZED: 'utc_normalized',
1018
- NO_TIMEZONE: 'no_timezone'
1019
- };
1020
- const DATETIME_SUBTYPES_LABELS = {
1021
- [DATETIME_SUBTYPES.DATE]: 'Date',
1022
- [DATETIME_SUBTYPES.TIME]: 'Time',
1023
- [DATETIME_SUBTYPES.DATETIME]: 'Date & Time'
1024
- };
1025
- const TIME_SERIALISINGFORMAT_LABELS = {
1026
- [TIME_SERIALISING_FORMATS.UTC_OFFSET]: 'UTC offset',
1027
- [TIME_SERIALISING_FORMATS.UTC_NORMALIZED]: 'UTC normalized',
1028
- [TIME_SERIALISING_FORMATS.NO_TIMEZONE]: 'No timezone'
1029
- };
1030
- const DATETIME_SUBTYPE_PATH = ['subtype'];
1031
- const DATE_LABEL_PATH = ['dateLabel'];
1032
- const DATE_DISALLOW_PAST_PATH = ['disallowPassedDates'];
1033
- const TIME_LABEL_PATH = ['timeLabel'];
1034
- const TIME_USE24H_PATH = ['use24h'];
1035
- const TIME_INTERVAL_PATH = ['timeInterval'];
1036
- const TIME_SERIALISING_FORMAT_PATH = ['timeSerializingFormat'];
1037
-
1038
- // config ///////////////////
1039
-
1040
- const VALUES_SOURCES = {
1041
- STATIC: 'static',
1042
- INPUT: 'input',
1043
- EXPRESSION: 'expression'
1044
- };
1045
- const VALUES_SOURCE_DEFAULT = VALUES_SOURCES.STATIC;
1046
- const VALUES_SOURCES_LABELS = {
1047
- [VALUES_SOURCES.STATIC]: 'Static',
1048
- [VALUES_SOURCES.INPUT]: 'Input data',
1049
- [VALUES_SOURCES.EXPRESSION]: 'Expression'
1050
- };
1051
- const VALUES_SOURCES_PATHS = {
1052
- [VALUES_SOURCES.STATIC]: ['values'],
1053
- [VALUES_SOURCES.INPUT]: ['valuesKey'],
1054
- [VALUES_SOURCES.EXPRESSION]: ['valuesExpression']
1055
- };
1056
- const VALUES_SOURCES_DEFAULTS = {
1057
- [VALUES_SOURCES.STATIC]: [{
1058
- label: 'Value',
1059
- value: 'value'
1060
- }],
1061
- [VALUES_SOURCES.INPUT]: '',
1062
- [VALUES_SOURCES.EXPRESSION]: '='
1063
- };
1064
-
1065
- // helpers ///////////////////
1066
-
1067
- function getValuesSource(field) {
1068
- for (const source of Object.values(VALUES_SOURCES)) {
1069
- if (minDash.get(field, VALUES_SOURCES_PATHS[source]) !== undefined) {
1070
- return source;
1071
- }
1072
- }
1073
- return VALUES_SOURCE_DEFAULT;
1074
- }
1075
-
1076
- function createInjector(bootstrapModules) {
1077
- const injector = new didi.Injector(bootstrapModules);
1078
- injector.init();
1079
- return injector;
1080
- }
1081
-
1082
- /**
1083
- * @param {string?} prefix
1084
- *
1085
- * @returns Element
1086
- */
1087
- function createFormContainer(prefix = 'fjs') {
1088
- const container = document.createElement('div');
1089
- container.classList.add(`${prefix}-container`);
1090
- return container;
1091
- }
1092
-
1093
- const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression'];
1094
- const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text'];
1095
- function findErrors(errors, path) {
1096
- return errors[pathStringify(path)];
1097
- }
1098
- function isRequired(field) {
1099
- return field.required;
1100
- }
1101
- function pathParse(path) {
1102
- if (!path) {
1103
- return [];
1104
- }
1105
- return path.split('.').map(key => {
1106
- return isNaN(parseInt(key)) ? key : parseInt(key);
1107
- });
1108
- }
1109
- function pathsEqual(a, b) {
1110
- return a && b && a.length === b.length && a.every((value, index) => value === b[index]);
1111
- }
1112
- function pathStringify(path) {
1113
- if (!path) {
1114
- return '';
1115
- }
1116
- return path.join('.');
1117
- }
1118
- const indices = {};
1119
- function generateIndexForType(type) {
1120
- if (type in indices) {
1121
- indices[type]++;
1122
- } else {
1123
- indices[type] = 1;
1124
- }
1125
- return indices[type];
1126
- }
1127
- function generateIdForType(type) {
1128
- return `${type}${generateIndexForType(type)}`;
1129
- }
1130
-
1131
- /**
1132
- * @template T
1133
- * @param {T} data
1134
- * @param {(this: any, key: string, value: any) => any} [replacer]
1135
- * @return {T}
1136
- */
1137
- function clone(data, replacer) {
1138
- return JSON.parse(JSON.stringify(data, replacer));
1139
- }
1140
-
1141
- /**
1142
- * Parse the schema for input variables a form might make use of
1143
- *
1144
- * @param {any} schema
1145
- *
1146
- * @return {string[]}
1147
- */
1148
- function getSchemaVariables(schema, options = {}) {
1149
- const {
1150
- expressionLanguage = new FeelExpressionLanguage(null),
1151
- templating = new FeelersTemplating(),
1152
- inputs = true,
1153
- outputs = true
1154
- } = options;
1155
- if (!schema.components) {
1156
- return [];
1157
- }
1158
- const variables = schema.components.reduce((variables, component) => {
1159
- const {
1160
- key,
1161
- valuesKey,
1162
- type
1163
- } = component;
1164
- if (['button'].includes(type)) {
1165
- return variables;
1166
- }
1167
-
1168
- // collect bi-directional variables
1169
- if (inputs || outputs) {
1170
- if (key) {
1171
- variables = [...variables, key];
1172
- }
1173
- }
1174
-
1175
- // collect input-only variables
1176
- if (inputs) {
1177
- if (valuesKey) {
1178
- variables = [...variables, valuesKey];
1179
- }
1180
- EXPRESSION_PROPERTIES.forEach(prop => {
1181
- const property = minDash.get(component, prop.split('.'));
1182
- if (property && expressionLanguage.isExpression(property)) {
1183
- const expressionVariables = expressionLanguage.getVariableNames(property, {
1184
- type: 'expression'
1185
- });
1186
- variables = [...variables, ...expressionVariables];
1187
- }
1188
- });
1189
- TEMPLATE_PROPERTIES.forEach(prop => {
1190
- const property = minDash.get(component, prop.split('.'));
1191
- if (property && !expressionLanguage.isExpression(property) && templating.isTemplate(property)) {
1192
- const templateVariables = templating.getVariableNames(property);
1193
- variables = [...variables, ...templateVariables];
1194
- }
1195
- });
1196
- }
1197
- return variables.filter(variable => variable !== undefined || variable !== null);
1198
- }, []);
1188
+ CommandStack.prototype._markDirty = function (elements) {
1189
+ const execution = this._currentExecution;
1190
+ if (!elements) {
1191
+ return;
1192
+ }
1193
+ elements = minDash.isArray(elements) ? elements : [elements];
1194
+ execution.dirty = execution.dirty.concat(elements);
1195
+ };
1196
+ CommandStack.prototype._executedAction = function (action, redo) {
1197
+ const stackIdx = ++this._stackIdx;
1198
+ if (!redo) {
1199
+ this._stack.splice(stackIdx, this._stack.length, action);
1200
+ }
1201
+ };
1202
+ CommandStack.prototype._revertedAction = function (action) {
1203
+ this._stackIdx--;
1204
+ };
1205
+ CommandStack.prototype._getHandler = function (command) {
1206
+ return this._handlerMap[command];
1207
+ };
1208
+ CommandStack.prototype._setHandler = function (command, handler) {
1209
+ if (!command || !handler) {
1210
+ throw new Error('command and handler required');
1211
+ }
1212
+ if (this._handlerMap[command]) {
1213
+ throw new Error('overriding handler for command <' + command + '>');
1214
+ }
1215
+ this._handlerMap[command] = handler;
1216
+ };
1199
1217
 
1200
- // remove duplicates
1201
- return Array.from(new Set(variables));
1202
- }
1218
+ /**
1219
+ * @type { import('didi').ModuleDeclaration }
1220
+ */
1221
+ var commandModule = {
1222
+ commandStack: ['type', CommandStack]
1223
+ };
1203
1224
 
1204
1225
  class UpdateFieldValidationHandler {
1205
1226
  constructor(form, validator) {
@@ -1211,15 +1232,12 @@ class UpdateFieldValidationHandler {
1211
1232
  field,
1212
1233
  value
1213
1234
  } = context;
1214
- const {
1215
- _path
1216
- } = field;
1217
1235
  const {
1218
1236
  errors
1219
1237
  } = this._form._getState();
1220
1238
  context.oldErrors = clone(errors);
1221
1239
  const fieldErrors = this._validator.validateField(field, value);
1222
- const updatedErrors = minDash.set(errors, [pathStringify(_path)], fieldErrors.length ? fieldErrors : undefined);
1240
+ const updatedErrors = minDash.set(errors, [field.id], fieldErrors.length ? fieldErrors : undefined);
1223
1241
  this._form._setState({
1224
1242
  errors: updatedErrors
1225
1243
  });
@@ -1902,54 +1920,397 @@ function evaluateFEELValues(validate, expressionLanguage, conditionChecker, form
1902
1920
  return evaluatedValidate;
1903
1921
  }
1904
1922
 
1905
- class FormFieldRegistry {
1906
- constructor(eventBus) {
1907
- this._eventBus = eventBus;
1908
- this._formFields = {};
1909
- eventBus.on('form.clear', () => this.clear());
1910
- this._ids = new Ids([32, 36, 1]);
1911
- this._keys = new Ids([32, 36, 1]);
1923
+ class Importer {
1924
+ /**
1925
+ * @constructor
1926
+ * @param { import('./FormFieldRegistry').default } formFieldRegistry
1927
+ * @param { import('./PathRegistry').default } pathRegistry
1928
+ * @param { import('./FieldFactory').default } fieldFactory
1929
+ * @param { import('./FormLayouter').default } formLayouter
1930
+ */
1931
+ constructor(formFieldRegistry, pathRegistry, fieldFactory, formLayouter) {
1932
+ this._formFieldRegistry = formFieldRegistry;
1933
+ this._pathRegistry = pathRegistry;
1934
+ this._fieldFactory = fieldFactory;
1935
+ this._formLayouter = formLayouter;
1912
1936
  }
1913
- add(formField) {
1937
+
1938
+ /**
1939
+ * Import schema creating rows, fields, attaching additional
1940
+ * information to each field and adding fields to the
1941
+ * field registry.
1942
+ *
1943
+ * Additional information attached:
1944
+ *
1945
+ * * `id` (unless present)
1946
+ * * `_parent`
1947
+ * * `_path`
1948
+ *
1949
+ * @param {any} schema
1950
+ *
1951
+ * @typedef {{ warnings: Error[], schema: any }} ImportResult
1952
+ * @returns {ImportResult}
1953
+ */
1954
+ importSchema(schema) {
1955
+ // TODO: Add warnings
1956
+ const warnings = [];
1957
+ try {
1958
+ this._cleanup();
1959
+ const importedSchema = this.importFormField(clone(schema));
1960
+ this._formLayouter.calculateLayout(clone(importedSchema));
1961
+ return {
1962
+ schema: importedSchema,
1963
+ warnings
1964
+ };
1965
+ } catch (err) {
1966
+ this._cleanup();
1967
+ err.warnings = warnings;
1968
+ throw err;
1969
+ }
1970
+ }
1971
+ _cleanup() {
1972
+ this._formLayouter.clear();
1973
+ this._formFieldRegistry.clear();
1974
+ this._pathRegistry.clear();
1975
+ }
1976
+
1977
+ /**
1978
+ * @param {{[x: string]: any}} fieldAttrs
1979
+ * @param {String} [parentId]
1980
+ * @param {number} [index]
1981
+ *
1982
+ * @return {any} field
1983
+ */
1984
+ importFormField(fieldAttrs, parentId, index) {
1914
1985
  const {
1915
- id
1916
- } = formField;
1917
- if (this._formFields[id]) {
1918
- throw new Error(`form field with ID ${id} already exists`);
1986
+ components
1987
+ } = fieldAttrs;
1988
+ let parent, path;
1989
+ if (parentId) {
1990
+ parent = this._formFieldRegistry.get(parentId);
1919
1991
  }
1920
- this._eventBus.fire('formField.add', {
1921
- formField
1992
+
1993
+ // set form field path
1994
+ path = parent ? [...parent._path, 'components', index] : [];
1995
+ const field = this._fieldFactory.create({
1996
+ ...fieldAttrs,
1997
+ _path: path,
1998
+ _parent: parentId
1999
+ }, false);
2000
+ this._formFieldRegistry.add(field);
2001
+ if (components) {
2002
+ field.components = this.importFormFields(components, field.id);
2003
+ }
2004
+ return field;
2005
+ }
2006
+
2007
+ /**
2008
+ * @param {Array<any>} components
2009
+ * @param {string} parentId
2010
+ *
2011
+ * @return {Array<any>} imported components
2012
+ */
2013
+ importFormFields(components, parentId) {
2014
+ return components.map((component, index) => {
2015
+ return this.importFormField(component, parentId, index);
1922
2016
  });
1923
- this._formFields[id] = formField;
1924
2017
  }
1925
- remove(formField) {
2018
+ }
2019
+ Importer.$inject = ['formFieldRegistry', 'pathRegistry', 'fieldFactory', 'formLayouter'];
2020
+
2021
+ class FieldFactory {
2022
+ /**
2023
+ * @constructor
2024
+ *
2025
+ * @param formFieldRegistry
2026
+ * @param formFields
2027
+ */
2028
+ constructor(formFieldRegistry, pathRegistry, formFields) {
2029
+ this._formFieldRegistry = formFieldRegistry;
2030
+ this._pathRegistry = pathRegistry;
2031
+ this._formFields = formFields;
2032
+ }
2033
+ create(attrs, applyDefaults = true) {
1926
2034
  const {
1927
- id
1928
- } = formField;
1929
- if (!this._formFields[id]) {
1930
- return;
2035
+ id,
2036
+ type,
2037
+ key,
2038
+ path,
2039
+ _parent
2040
+ } = attrs;
2041
+ const fieldDefinition = this._formFields.get(type);
2042
+ if (!fieldDefinition) {
2043
+ throw new Error(`form field of type <${type}> not supported`);
1931
2044
  }
1932
- this._eventBus.fire('formField.remove', {
1933
- formField
2045
+ const {
2046
+ config
2047
+ } = fieldDefinition;
2048
+ if (!config) {
2049
+ throw new Error(`form field of type <${type}> has no config`);
2050
+ }
2051
+ if (id && this._formFieldRegistry._ids.assigned(id)) {
2052
+ throw new Error(`form field with id <${id}> already exists`);
2053
+ }
2054
+
2055
+ // ensure that we can claim the path
2056
+
2057
+ const parent = _parent && this._formFieldRegistry.get(_parent);
2058
+ const parentPath = parent && this._pathRegistry.getValuePath(parent) || [];
2059
+ if (config.keyed && key && !this._pathRegistry.canClaimPath([...parentPath, ...key.split('.')], true)) {
2060
+ throw new Error(`binding path '${[...parentPath, key].join('.')}' is already claimed`);
2061
+ }
2062
+ if (config.pathed && path && !this._pathRegistry.canClaimPath([...parentPath, ...path.split('.')], false)) {
2063
+ throw new Error(`binding path '${[...parentPath, ...path.split('.')].join('.')}' is already claimed`);
2064
+ }
2065
+ const labelAttrs = applyDefaults && config.label ? {
2066
+ label: config.label
2067
+ } : {};
2068
+ const field = config.create({
2069
+ ...labelAttrs,
2070
+ ...attrs
1934
2071
  });
1935
- delete this._formFields[id];
2072
+ this._ensureId(field);
2073
+ if (config.keyed) {
2074
+ this._ensureKey(field);
2075
+ }
2076
+ if (config.pathed && path) {
2077
+ this._pathRegistry.claimPath(this._pathRegistry.getValuePath(field), false);
2078
+ }
2079
+ return field;
1936
2080
  }
1937
- get(id) {
1938
- return this._formFields[id];
2081
+ _ensureId(field) {
2082
+ if (field.id) {
2083
+ this._formFieldRegistry._ids.claim(field.id, field);
2084
+ return;
2085
+ }
2086
+ let prefix = 'Field';
2087
+ if (field.type === 'default') {
2088
+ prefix = 'Form';
2089
+ }
2090
+ field.id = this._formFieldRegistry._ids.nextPrefixed(`${prefix}_`, field);
1939
2091
  }
1940
- getAll() {
1941
- return Object.values(this._formFields);
2092
+ _ensureKey(field) {
2093
+ if (!field.key) {
2094
+ let random;
2095
+ const parent = this._formFieldRegistry.get(field._parent);
2096
+
2097
+ // ensure key uniqueness at level
2098
+ do {
2099
+ random = Math.random().toString(36).substring(7);
2100
+ } while (parent && parent.components.some(child => child.key === random));
2101
+ field.key = `${field.type}_${random}`;
2102
+ }
2103
+ this._pathRegistry.claimPath(this._pathRegistry.getValuePath(field), true);
1942
2104
  }
1943
- forEach(callback) {
1944
- this.getAll().forEach(formField => callback(formField));
2105
+ }
2106
+ FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields'];
2107
+
2108
+ /**
2109
+ * The PathRegistry class manages a hierarchical structure of paths associated with form fields.
2110
+ * It enables claiming, unclaiming, and validating paths within this structure.
2111
+ *
2112
+ * Example Tree Structure:
2113
+ *
2114
+ * [
2115
+ * {
2116
+ * segment: 'root',
2117
+ * claimCount: 1,
2118
+ * children: [
2119
+ * {
2120
+ * segment: 'child1',
2121
+ * claimCount: 2,
2122
+ * children: null // A leaf node (closed path)
2123
+ * },
2124
+ * {
2125
+ * segment: 'child2',
2126
+ * claimCount: 1,
2127
+ * children: [
2128
+ * {
2129
+ * segment: 'subChild1',
2130
+ * claimCount: 1,
2131
+ * children: [] // An open node (open path)
2132
+ * }
2133
+ * ]
2134
+ * }
2135
+ * ]
2136
+ * }
2137
+ * ]
2138
+ */
2139
+ class PathRegistry {
2140
+ constructor(formFieldRegistry, formFields) {
2141
+ this._formFieldRegistry = formFieldRegistry;
2142
+ this._formFields = formFields;
2143
+ this._dataPaths = [];
2144
+ }
2145
+ canClaimPath(path, closed = false) {
2146
+ let node = {
2147
+ children: this._dataPaths
2148
+ };
2149
+ for (const segment of path) {
2150
+ node = _getNextSegment(node, segment);
2151
+
2152
+ // if no node at that path, we can claim it no matter what
2153
+ if (!node) {
2154
+ return true;
2155
+ }
2156
+
2157
+ // if we reach a leaf node, definitely not claimable
2158
+ if (node.children === null) {
2159
+ return false;
2160
+ }
2161
+ }
2162
+
2163
+ // if after all segments we reach a node with children, we can claim it only openly
2164
+ return !closed;
2165
+ }
2166
+ claimPath(path, closed = false) {
2167
+ if (!this.canClaimPath(path, closed)) {
2168
+ throw new Error(`cannot claim path '${path.join('.')}'`);
2169
+ }
2170
+ let node = {
2171
+ children: this._dataPaths
2172
+ };
2173
+ for (const segment of path) {
2174
+ let child = _getNextSegment(node, segment);
2175
+ if (!child) {
2176
+ child = {
2177
+ segment,
2178
+ claimCount: 1,
2179
+ children: []
2180
+ };
2181
+ node.children.push(child);
2182
+ } else {
2183
+ child.claimCount++;
2184
+ }
2185
+ node = child;
2186
+ }
2187
+ if (closed) {
2188
+ node.children = null;
2189
+ }
2190
+ }
2191
+ unclaimPath(path) {
2192
+ // verification Pass
2193
+ let node = {
2194
+ children: this._dataPaths
2195
+ };
2196
+ for (const segment of path) {
2197
+ const child = _getNextSegment(node, segment);
2198
+ if (!child) {
2199
+ throw new Error(`no open path found for '${path.join('.')}'`);
2200
+ }
2201
+ node = child;
2202
+ }
2203
+
2204
+ // mutation Pass
2205
+ node = {
2206
+ children: this._dataPaths
2207
+ };
2208
+ for (const segment of path) {
2209
+ const child = _getNextSegment(node, segment);
2210
+ child.claimCount--;
2211
+ if (child.claimCount === 0) {
2212
+ node.children.splice(node.children.indexOf(child), 1);
2213
+ break; // Abort early if claimCount reaches zero
2214
+ }
2215
+
2216
+ node = child;
2217
+ }
2218
+ }
2219
+
2220
+ /**
2221
+ * Applies a function (fn) recursively on a given field and its children.
2222
+ *
2223
+ * - `field`: Starting field object.
2224
+ * - `fn`: Function to apply.
2225
+ * - `context`: Optional object for passing data between calls.
2226
+ *
2227
+ * Stops early if `fn` returns `false`. Useful for traversing the form field tree.
2228
+ *
2229
+ * @returns {boolean} Success status based on function execution.
2230
+ */
2231
+ executeRecursivelyOnFields(field, fn, context = {}) {
2232
+ let result = true;
2233
+ const formFieldConfig = this._formFields.get(field.type).config;
2234
+ if (formFieldConfig.keyed) {
2235
+ const callResult = fn({
2236
+ field,
2237
+ isClosed: true,
2238
+ context
2239
+ });
2240
+ return result && callResult;
2241
+ } else if (formFieldConfig.pathed) {
2242
+ const callResult = fn({
2243
+ field,
2244
+ isClosed: false,
2245
+ context
2246
+ });
2247
+ result = result && callResult;
2248
+ }
2249
+ if (field.components) {
2250
+ for (const child of field.components) {
2251
+ const callResult = this.executeRecursivelyOnFields(child, fn, clone(context));
2252
+ result = result && callResult;
2253
+
2254
+ // only stop executing if false is specifically returned, not if undefined
2255
+ if (result === false) {
2256
+ return result;
2257
+ }
2258
+ }
2259
+ }
2260
+ return result;
2261
+ }
2262
+
2263
+ /**
2264
+ * Generates an array representing the binding path to an underlying data object for a form field.
2265
+ *
2266
+ * @param {Object} field - The field object with properties: `key`, `path`, `id`, and optionally `_parent`.
2267
+ * @param {Object} [options={}] - Configuration options.
2268
+ * @param {Object} [options.replacements={}] - A map of field IDs to alternative path arrays.
2269
+ * @param {Object} [options.cutoffNode] - The ID of the parent field at which to stop generating the path.
2270
+ *
2271
+ * @returns {(Array<string>|undefined)} An array of strings representing the binding path, or undefined if not determinable.
2272
+ */
2273
+ getValuePath(field, options = {}) {
2274
+ const {
2275
+ replacements = {},
2276
+ cutoffNode = null
2277
+ } = options;
2278
+ let localValuePath = [];
2279
+ const hasReplacement = Object.prototype.hasOwnProperty.call(replacements, field.id);
2280
+ const formFieldConfig = this._formFields.get(field.type).config;
2281
+ if (hasReplacement) {
2282
+ const replacement = replacements[field.id];
2283
+ if (replacement === null || replacement === undefined || replacement === '') {
2284
+ localValuePath = [];
2285
+ } else if (typeof replacement === 'string') {
2286
+ localValuePath = replacement.split('.');
2287
+ } else if (Array.isArray(replacement)) {
2288
+ localValuePath = replacement;
2289
+ } else {
2290
+ throw new Error(`replacements for field ${field.id} must be a string, array or null/undefined`);
2291
+ }
2292
+ } else if (formFieldConfig.keyed) {
2293
+ localValuePath = field.key.split('.');
2294
+ } else if (formFieldConfig.pathed && field.path) {
2295
+ localValuePath = field.path.split('.');
2296
+ }
2297
+ if (field._parent && field._parent !== cutoffNode) {
2298
+ const parent = this._formFieldRegistry.get(field._parent);
2299
+ return [...(this.getValuePath(parent, options) || []), ...localValuePath];
2300
+ }
2301
+ return localValuePath;
1945
2302
  }
1946
2303
  clear() {
1947
- this._formFields = {};
1948
- this._ids.clear();
1949
- this._keys.clear();
2304
+ this._dataPaths = [];
1950
2305
  }
1951
2306
  }
1952
- FormFieldRegistry.$inject = ['eventBus'];
2307
+ const _getNextSegment = (node, segment) => {
2308
+ if (minDash.isArray(node.children)) {
2309
+ return node.children.find(node => node.segment === segment) || null;
2310
+ }
2311
+ return null;
2312
+ };
2313
+ PathRegistry.$inject = ['formFieldRegistry', 'formFields'];
1953
2314
 
1954
2315
  /**
1955
2316
  * @typedef { { id: String, components: Array<String> } } FormRow
@@ -2041,7 +2402,7 @@ class FormLayouter {
2041
2402
  type,
2042
2403
  components
2043
2404
  } = formField;
2044
- if (type !== 'default' || !components) {
2405
+ if (type !== 'default' && type !== 'group' || !components) {
2045
2406
  return;
2046
2407
  }
2047
2408
 
@@ -2096,147 +2457,52 @@ function allRows(formRows) {
2096
2457
  return minDash.flatten(formRows.map(c => c.rows));
2097
2458
  }
2098
2459
 
2099
- class Importer {
2100
- /**
2101
- * @constructor
2102
- * @param { import('../core').FormFieldRegistry } formFieldRegistry
2103
- * @param { import('../render/FormFields').default } formFields
2104
- * @param { import('../core').FormLayouter } formLayouter
2105
- */
2106
- constructor(formFieldRegistry, formFields, formLayouter) {
2107
- this._formFieldRegistry = formFieldRegistry;
2108
- this._formFields = formFields;
2109
- this._formLayouter = formLayouter;
2110
- }
2111
-
2112
- /**
2113
- * Import schema adding `id`, `_parent` and `_path`
2114
- * information to each field and adding it to the
2115
- * form field registry.
2116
- *
2117
- * @param {any} schema
2118
- * @param {any} [data]
2119
- *
2120
- * @return { { warnings: Array<any>, schema: any, data: any } }
2121
- */
2122
- importSchema(schema, data = {}) {
2123
- // TODO: Add warnings - https://github.com/bpmn-io/form-js/issues/289
2124
- const warnings = [];
2125
- try {
2126
- this._formLayouter.clear();
2127
- const importedSchema = this.importFormField(clone(schema)),
2128
- initializedData = this.initializeFieldValues(clone(data));
2129
- this._formLayouter.calculateLayout(clone(importedSchema));
2130
- return {
2131
- warnings,
2132
- schema: importedSchema,
2133
- data: initializedData
2134
- };
2135
- } catch (err) {
2136
- err.warnings = warnings;
2137
- throw err;
2138
- }
2460
+ class FormFieldRegistry {
2461
+ constructor(eventBus) {
2462
+ this._eventBus = eventBus;
2463
+ this._formFields = {};
2464
+ eventBus.on('form.clear', () => this.clear());
2465
+ this._ids = new Ids([32, 36, 1]);
2139
2466
  }
2140
-
2141
- /**
2142
- * @param {any} formField
2143
- * @param {string} [parentId]
2144
- *
2145
- * @return {any} importedField
2146
- */
2147
- importFormField(formField, parentId) {
2467
+ add(formField) {
2148
2468
  const {
2149
- components,
2150
- key,
2151
- type,
2152
- id = generateIdForType(type)
2153
- } = formField;
2154
- if (parentId) {
2155
- // set form field parent
2156
- formField._parent = parentId;
2157
- }
2158
- if (!this._formFields.get(type)) {
2159
- throw new Error(`form field of type <${type}> not supported`);
2160
- }
2161
- if (key) {
2162
- // validate <key> uniqueness
2163
- if (this._formFieldRegistry._keys.assigned(key)) {
2164
- throw new Error(`form field with key <${key}> already exists`);
2165
- }
2166
- this._formFieldRegistry._keys.claim(key, formField);
2167
-
2168
- // TODO: buttons should not have key
2169
- if (type !== 'button') {
2170
- // set form field path
2171
- formField._path = [key];
2172
- }
2173
- }
2174
- if (id) {
2175
- // validate <id> uniqueness
2176
- if (this._formFieldRegistry._ids.assigned(id)) {
2177
- throw new Error(`form field with id <${id}> already exists`);
2178
- }
2179
- this._formFieldRegistry._ids.claim(id, formField);
2180
- }
2181
-
2182
- // set form field ID
2183
- formField.id = id;
2184
- this._formFieldRegistry.add(formField);
2185
- if (components) {
2186
- this.importFormFields(components, id);
2469
+ id
2470
+ } = formField;
2471
+ if (this._formFields[id]) {
2472
+ throw new Error(`form field with ID ${id} already exists`);
2187
2473
  }
2188
- return formField;
2474
+ this._eventBus.fire('formField.add', {
2475
+ formField
2476
+ });
2477
+ this._formFields[id] = formField;
2189
2478
  }
2190
- importFormFields(components, parentId) {
2191
- components.forEach(component => {
2192
- this.importFormField(component, parentId);
2479
+ remove(formField) {
2480
+ const {
2481
+ id
2482
+ } = formField;
2483
+ if (!this._formFields[id]) {
2484
+ return;
2485
+ }
2486
+ this._eventBus.fire('formField.remove', {
2487
+ formField
2193
2488
  });
2489
+ delete this._formFields[id];
2194
2490
  }
2195
-
2196
- /**
2197
- * @param {Object} data
2198
- *
2199
- * @return {Object} initializedData
2200
- */
2201
- initializeFieldValues(data) {
2202
- return this._formFieldRegistry.getAll().reduce((initializedData, formField) => {
2203
- const {
2204
- defaultValue,
2205
- _path,
2206
- type
2207
- } = formField;
2208
-
2209
- // try to get value from data
2210
- // if unavailable - try to get default value from form field
2211
- // if unavailable - get empty value from form field
2212
-
2213
- if (_path) {
2214
- const {
2215
- config: fieldConfig
2216
- } = this._formFields.get(type);
2217
- let valueData = minDash.get(data, _path);
2218
- if (!minDash.isUndefined(valueData) && fieldConfig.sanitizeValue) {
2219
- valueData = fieldConfig.sanitizeValue({
2220
- formField,
2221
- data,
2222
- value: valueData
2223
- });
2224
- }
2225
- const initializedFieldValue = !minDash.isUndefined(valueData) ? valueData : !minDash.isUndefined(defaultValue) ? defaultValue : fieldConfig.emptyValue;
2226
- initializedData = {
2227
- ...initializedData,
2228
- [_path[0]]: initializedFieldValue
2229
- };
2230
- }
2231
- return initializedData;
2232
- }, data);
2491
+ get(id) {
2492
+ return this._formFields[id];
2493
+ }
2494
+ getAll() {
2495
+ return Object.values(this._formFields);
2496
+ }
2497
+ forEach(callback) {
2498
+ this.getAll().forEach(formField => callback(formField));
2499
+ }
2500
+ clear() {
2501
+ this._formFields = {};
2502
+ this._ids.clear();
2233
2503
  }
2234
2504
  }
2235
- Importer.$inject = ['formFieldRegistry', 'formFields', 'formLayouter'];
2236
-
2237
- var importModule = {
2238
- importer: ['type', Importer]
2239
- };
2505
+ FormFieldRegistry.$inject = ['eventBus'];
2240
2506
 
2241
2507
  function formFieldClasses(type, {
2242
2508
  errors = [],
@@ -2291,7 +2557,7 @@ function Button(props) {
2291
2557
  }
2292
2558
  Button.config = {
2293
2559
  type: type$c,
2294
- keyed: true,
2560
+ keyed: false,
2295
2561
  label: 'Button',
2296
2562
  group: 'action',
2297
2563
  create: (options = {}) => ({
@@ -2301,6 +2567,9 @@ Button.config = {
2301
2567
  };
2302
2568
 
2303
2569
  const FormRenderContext = preact.createContext({
2570
+ EmptyRoot: props => {
2571
+ return null;
2572
+ },
2304
2573
  Empty: props => {
2305
2574
  return null;
2306
2575
  },
@@ -2330,6 +2599,10 @@ const FormRenderContext = preact.createContext({
2330
2599
  class: props.class,
2331
2600
  children: props.children
2332
2601
  });
2602
+ },
2603
+ hoveredId: [],
2604
+ setHoveredId: newValue => {
2605
+ console.log(`setHoveredId not defined, called with '${newValue}'`);
2333
2606
  }
2334
2607
  });
2335
2608
  var FormRenderContext$1 = FormRenderContext;
@@ -2451,6 +2724,37 @@ function useReadonly(formField, properties = {}) {
2451
2724
  return readonly || false;
2452
2725
  }
2453
2726
 
2727
+ function usePrevious(value, defaultValue, dependencies) {
2728
+ const ref = hooks.useRef(defaultValue);
2729
+ hooks.useEffect(() => ref.current = value, dependencies);
2730
+ return ref.current;
2731
+ }
2732
+
2733
+ /**
2734
+ * A custom hook to manage state changes with deep comparison.
2735
+ *
2736
+ * @param {any} value - The current value to manage.
2737
+ * @param {any} defaultValue - The initial default value for the state.
2738
+ * @returns {any} - Returns the current state.
2739
+ */
2740
+ function useDeepCompareState(value, defaultValue) {
2741
+ const [state, setState] = hooks.useState(defaultValue);
2742
+ const previous = usePrevious(value, defaultValue, [value]);
2743
+ const changed = !compare(previous, value);
2744
+ hooks.useEffect(() => {
2745
+ if (changed) {
2746
+ setState(value);
2747
+ }
2748
+ }, [changed, value]);
2749
+ return state;
2750
+ }
2751
+
2752
+ // helpers //////////////////////////
2753
+
2754
+ function compare(a, b) {
2755
+ return JSON.stringify(a) === JSON.stringify(b);
2756
+ }
2757
+
2454
2758
  /**
2455
2759
  * Template a string reactively based on form data. If the string is not a template, it is returned as is.
2456
2760
  * Memoised to minimize re-renders
@@ -2671,6 +2975,21 @@ function _isReadableType(value) {
2671
2975
  function _isValueSomething(value) {
2672
2976
  return value || value === 0 || value === false;
2673
2977
  }
2978
+ function createEmptyOptions(options = {}) {
2979
+ const defaults = {};
2980
+
2981
+ // provide default values if valuesKey and valuesExpression are not set
2982
+ if (!options.valuesKey && !options.valuesExpression) {
2983
+ defaults.values = [{
2984
+ label: 'Value',
2985
+ value: 'value'
2986
+ }];
2987
+ }
2988
+ return {
2989
+ ...defaults,
2990
+ ...options
2991
+ };
2992
+ }
2674
2993
 
2675
2994
  /**
2676
2995
  * @enum { String }
@@ -2705,11 +3024,8 @@ function useValuesAsync (field) {
2705
3024
  state: LOAD_STATES.LOADING
2706
3025
  });
2707
3026
  const initialData = useService('form')._getState().initialData;
2708
- const evaluatedValues = hooks.useMemo(() => {
2709
- if (valuesExpression) {
2710
- return useExpressionEvaluation(valuesExpression);
2711
- }
2712
- }, [valuesExpression]);
3027
+ const expressionEvaluation = useExpressionEvaluation(valuesExpression);
3028
+ const evaluatedValues = useDeepCompareState(expressionEvaluation || [], []);
2713
3029
  hooks.useEffect(() => {
2714
3030
  let values = [];
2715
3031
 
@@ -2725,8 +3041,10 @@ function useValuesAsync (field) {
2725
3041
  values = Array.isArray(staticValues) ? staticValues : [];
2726
3042
 
2727
3043
  // expression
2728
- } else if (evaluatedValues && Array.isArray(evaluatedValues)) {
2729
- values = evaluatedValues;
3044
+ } else if (valuesExpression) {
3045
+ if (evaluatedValues && Array.isArray(evaluatedValues)) {
3046
+ values = evaluatedValues;
3047
+ }
2730
3048
  } else {
2731
3049
  setValuesGetter(buildErrorState('No values source defined in the form definition'));
2732
3050
  return;
@@ -2735,7 +3053,7 @@ function useValuesAsync (field) {
2735
3053
  // normalize data to support primitives and partially defined objects
2736
3054
  values = normalizeValuesData(values);
2737
3055
  setValuesGetter(buildLoadedState(values));
2738
- }, [valuesKey, staticValues, initialData]);
3056
+ }, [valuesKey, staticValues, initialData, valuesExpression, evaluatedValues]);
2739
3057
  return valuesGetter;
2740
3058
  }
2741
3059
  const buildErrorState = error => ({
@@ -3047,21 +3365,7 @@ Checklist.config = {
3047
3365
  group: 'selection',
3048
3366
  emptyValue: [],
3049
3367
  sanitizeValue: sanitizeMultiSelectValue,
3050
- create: (options = {}) => {
3051
- const defaults = {};
3052
-
3053
- // provide default values if valuesKey isn't set
3054
- if (!options.valuesKey) {
3055
- defaults.values = [{
3056
- label: 'Value',
3057
- value: 'value'
3058
- }];
3059
- }
3060
- return {
3061
- ...defaults,
3062
- ...options
3063
- };
3064
- }
3368
+ create: createEmptyOptions
3065
3369
  };
3066
3370
 
3067
3371
  const noop$1 = () => false;
@@ -3072,6 +3376,7 @@ function FormField(props) {
3072
3376
  } = props;
3073
3377
  const formFields = useService('formFields'),
3074
3378
  viewerCommands = useService('viewerCommands', false),
3379
+ pathRegistry = useService('pathRegistry'),
3075
3380
  form = useService('form');
3076
3381
  const {
3077
3382
  initialData,
@@ -3088,10 +3393,10 @@ function FormField(props) {
3088
3393
  if (!FormFieldComponent) {
3089
3394
  throw new Error(`cannot render field <${field.type}>`);
3090
3395
  }
3091
- const initialValue = hooks.useMemo(() => minDash.get(initialData, field._path), [initialData, field._path]);
3092
- const value = minDash.get(data, field._path);
3093
- const fieldErrors = findErrors(errors, field._path);
3396
+ const valuePath = hooks.useMemo(() => pathRegistry.getValuePath(field), [field, pathRegistry]);
3397
+ const initialValue = hooks.useMemo(() => minDash.get(initialData, valuePath), [initialData, valuePath]);
3094
3398
  const readonly = useReadonly(field, properties);
3399
+ const value = minDash.get(data, valuePath);
3095
3400
 
3096
3401
  // add precedence: global readonly > form field disabled
3097
3402
  const disabled = !properties.readOnly && (properties.disabled || field.disabled || false);
@@ -3118,7 +3423,7 @@ function FormField(props) {
3118
3423
  children: jsxRuntime.jsx(FormFieldComponent, {
3119
3424
  ...props,
3120
3425
  disabled: disabled,
3121
- errors: fieldErrors,
3426
+ errors: errors[field.id],
3122
3427
  onChange: disabled || readonly ? noop$1 : onChange,
3123
3428
  onBlur: disabled || readonly ? noop$1 : onBlur,
3124
3429
  readonly: readonly,
@@ -3128,14 +3433,14 @@ function FormField(props) {
3128
3433
  });
3129
3434
  }
3130
3435
 
3131
- function Default(props) {
3436
+ function Grid(props) {
3132
3437
  const {
3133
3438
  Children,
3134
- Empty,
3135
3439
  Row
3136
3440
  } = hooks.useContext(FormRenderContext$1);
3137
3441
  const {
3138
- field
3442
+ field,
3443
+ Empty
3139
3444
  } = props;
3140
3445
  const {
3141
3446
  id,
@@ -3172,7 +3477,20 @@ function Default(props) {
3172
3477
  }), components.length ? null : jsxRuntime.jsx(Empty, {})]
3173
3478
  });
3174
3479
  }
3175
- Default.config = {
3480
+
3481
+ function FormComponent$1(props) {
3482
+ const {
3483
+ EmptyRoot
3484
+ } = hooks.useContext(FormRenderContext$1);
3485
+ const fullProps = {
3486
+ ...props,
3487
+ Empty: EmptyRoot
3488
+ };
3489
+ return jsxRuntime.jsx(Grid, {
3490
+ ...fullProps
3491
+ });
3492
+ }
3493
+ FormComponent$1.config = {
3176
3494
  type: 'default',
3177
3495
  keyed: false,
3178
3496
  label: null,
@@ -3201,6 +3519,74 @@ var SvgCalendar = function SvgCalendar(props) {
3201
3519
  };
3202
3520
  var CalendarIcon = SvgCalendar;
3203
3521
 
3522
+ /**
3523
+ * Returns date format for the provided locale.
3524
+ * If the locale is not provided, uses the browser's locale.
3525
+ *
3526
+ * @param {string} [locale] - The locale to get date format for.
3527
+ * @returns {string} The date format for the locale.
3528
+ */
3529
+ function getLocaleDateFormat(locale = 'default') {
3530
+ const parts = new Intl.DateTimeFormat(locale).formatToParts(new Date(Date.UTC(2020, 5, 5)));
3531
+ return parts.map(part => {
3532
+ const len = part.value.length;
3533
+ switch (part.type) {
3534
+ case 'day':
3535
+ return 'd'.repeat(len);
3536
+ case 'month':
3537
+ return 'M'.repeat(len);
3538
+ case 'year':
3539
+ return 'y'.repeat(len);
3540
+ default:
3541
+ return part.value;
3542
+ }
3543
+ }).join('');
3544
+ }
3545
+
3546
+ /**
3547
+ * Returns readable date format for the provided locale.
3548
+ * If the locale is not provided, uses the browser's locale.
3549
+ *
3550
+ * @param {string} [locale] - The locale to get readable date format for.
3551
+ * @returns {string} The readable date format for the locale.
3552
+ */
3553
+ function getLocaleReadableDateFormat(locale) {
3554
+ let format = getLocaleDateFormat(locale).toLowerCase();
3555
+
3556
+ // Ensure month is in 'mm' format
3557
+ if (!format.includes('mm')) {
3558
+ format = format.replace('m', 'mm');
3559
+ }
3560
+
3561
+ // Ensure day is in 'dd' format
3562
+ if (!format.includes('dd')) {
3563
+ format = format.replace('d', 'dd');
3564
+ }
3565
+ return format;
3566
+ }
3567
+
3568
+ /**
3569
+ * Returns flatpickr config for the provided locale.
3570
+ * If the locale is not provided, uses the browser's locale.
3571
+ *
3572
+ * @param {string} [locale] - The locale to get flatpickr config for.
3573
+ * @returns {object} The flatpickr config for the locale.
3574
+ */
3575
+ function getLocaleDateFlatpickrConfig(locale) {
3576
+ return flatpickerizeDateFormat(getLocaleDateFormat(locale));
3577
+ }
3578
+ function flatpickerizeDateFormat(dateFormat) {
3579
+ const useLeadingZero = {
3580
+ day: dateFormat.includes('dd'),
3581
+ month: dateFormat.includes('MM'),
3582
+ year: dateFormat.includes('yyyy')
3583
+ };
3584
+ dateFormat = useLeadingZero.day ? dateFormat.replace('dd', 'd') : dateFormat.replace('d', 'j');
3585
+ dateFormat = useLeadingZero.month ? dateFormat.replace('MM', 'm') : dateFormat.replace('M', 'n');
3586
+ dateFormat = useLeadingZero.year ? dateFormat.replace('yyyy', 'Y') : dateFormat.replace('yy', 'y');
3587
+ return dateFormat;
3588
+ }
3589
+
3204
3590
  function InputAdorner(props) {
3205
3591
  const {
3206
3592
  pre,
@@ -3275,7 +3661,7 @@ function Datepicker(props) {
3275
3661
  hooks.useEffect(() => {
3276
3662
  let config = {
3277
3663
  allowInput: true,
3278
- dateFormat: 'm/d/Y',
3664
+ dateFormat: getLocaleDateFlatpickrConfig(),
3279
3665
  static: true,
3280
3666
  clickOpens: false,
3281
3667
  // TODO: support dates prior to 1900 (https://github.com/bpmn-io/form-js/issues/533)
@@ -3367,7 +3753,7 @@ function Datepicker(props) {
3367
3753
  class: "fjs-input",
3368
3754
  disabled: disabled,
3369
3755
  readOnly: readonly,
3370
- placeholder: "mm/dd/yyyy",
3756
+ placeholder: getLocaleReadableDateFormat(),
3371
3757
  autoComplete: "off",
3372
3758
  onFocus: onInputFocus,
3373
3759
  onKeyDown: onInputKeyDown,
@@ -3452,7 +3838,8 @@ function DropdownList(props) {
3452
3838
  hooks.useEffect(() => {
3453
3839
  const individualEntries = dropdownContainer.current.children;
3454
3840
  if (individualEntries.length && !mouseControl) {
3455
- individualEntries[focusedValueIndex].scrollIntoView({
3841
+ const focusedEntry = individualEntries[focusedValueIndex];
3842
+ focusedEntry && focusedEntry.scrollIntoView({
3456
3843
  block: 'nearest',
3457
3844
  inline: 'nearest'
3458
3845
  });
@@ -3988,6 +4375,52 @@ function FormComponent(props) {
3988
4375
  });
3989
4376
  }
3990
4377
 
4378
+ function Group(props) {
4379
+ const {
4380
+ field
4381
+ } = props;
4382
+ const {
4383
+ label,
4384
+ id,
4385
+ type,
4386
+ showOutline
4387
+ } = field;
4388
+ const {
4389
+ formId
4390
+ } = hooks.useContext(FormContext$1);
4391
+ const {
4392
+ Empty
4393
+ } = hooks.useContext(FormRenderContext$1);
4394
+ const fullProps = {
4395
+ ...props,
4396
+ Empty
4397
+ };
4398
+ return jsxRuntime.jsxs("div", {
4399
+ className: classNames(formFieldClasses(type), {
4400
+ 'fjs-outlined': showOutline
4401
+ }),
4402
+ role: "group",
4403
+ "aria-labelledby": prefixId(id, formId),
4404
+ children: [jsxRuntime.jsx(Label, {
4405
+ id: prefixId(id, formId),
4406
+ label: label
4407
+ }), jsxRuntime.jsx(Grid, {
4408
+ ...fullProps
4409
+ })]
4410
+ });
4411
+ }
4412
+ Group.config = {
4413
+ type: 'group',
4414
+ pathed: true,
4415
+ label: 'Group',
4416
+ group: 'presentation',
4417
+ create: (options = {}) => ({
4418
+ components: [],
4419
+ showOutline: true,
4420
+ ...options
4421
+ })
4422
+ };
4423
+
3991
4424
  const NODE_TYPE_TEXT = 3,
3992
4425
  NODE_TYPE_ELEMENT = 1;
3993
4426
  const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong', 'table', 'thead', 'tbody', 'tr', 'th', 'td'];
@@ -4576,21 +5009,7 @@ Radio.config = {
4576
5009
  group: 'selection',
4577
5010
  emptyValue: null,
4578
5011
  sanitizeValue: sanitizeSingleSelectValue,
4579
- create: (options = {}) => {
4580
- const defaults = {};
4581
-
4582
- // provide default values if valuesKey isn't set
4583
- if (!options.valuesKey) {
4584
- defaults.values = [{
4585
- label: 'Value',
4586
- value: 'value'
4587
- }];
4588
- }
4589
- return {
4590
- ...defaults,
4591
- ...options
4592
- };
4593
- }
5012
+ create: createEmptyOptions
4594
5013
  };
4595
5014
 
4596
5015
  var _path$d;
@@ -4942,21 +5361,7 @@ Select.config = {
4942
5361
  group: 'selection',
4943
5362
  emptyValue: null,
4944
5363
  sanitizeValue: sanitizeSingleSelectValue,
4945
- create: (options = {}) => {
4946
- const defaults = {};
4947
-
4948
- // provide default values if valuesKey isn't set
4949
- if (!options.valuesKey) {
4950
- defaults.values = [{
4951
- label: 'Value',
4952
- value: 'value'
4953
- }];
4954
- }
4955
- return {
4956
- ...defaults,
4957
- ...options
4958
- };
4959
- }
5364
+ create: createEmptyOptions
4960
5365
  };
4961
5366
 
4962
5367
  const type$4 = 'spacer';
@@ -5184,21 +5589,7 @@ Taglist.config = {
5184
5589
  group: 'selection',
5185
5590
  emptyValue: [],
5186
5591
  sanitizeValue: sanitizeMultiSelectValue,
5187
- create: (options = {}) => {
5188
- const defaults = {};
5189
-
5190
- // provide default values if valuesKey isn't set
5191
- if (!options.valuesKey) {
5192
- defaults.values = [{
5193
- label: 'Value',
5194
- value: 'value'
5195
- }];
5196
- }
5197
- return {
5198
- ...defaults,
5199
- ...options
5200
- };
5201
- }
5592
+ create: createEmptyOptions
5202
5593
  };
5203
5594
 
5204
5595
  const type$2 = 'text';
@@ -5615,13 +6006,14 @@ var SvgGroup = function SvgGroup(props) {
5615
6006
  return /*#__PURE__*/React__namespace.createElement("svg", _extends$8({
5616
6007
  xmlns: "http://www.w3.org/2000/svg",
5617
6008
  width: 54,
5618
- height: 54
6009
+ height: 54,
6010
+ fill: "currentcolor"
5619
6011
  }, props), _path$8 || (_path$8 = /*#__PURE__*/React__namespace.createElement("path", {
5620
6012
  fillRule: "evenodd",
5621
6013
  d: "M8 33v5a1 1 0 0 0 1 1h4v2H9a3 3 0 0 1-3-3v-5h2Zm18 6v2H15v-2h11Zm13 0v2H28v-2h11Zm9-6v5a3 3 0 0 1-3 3h-4v-2h4a1 1 0 0 0 .993-.883L46 38v-5h2ZM8 22v9H6v-9h2Zm40 0v9h-2v-9h2Zm-35-9v2H9a1 1 0 0 0-.993.883L8 16v4H6v-4a3 3 0 0 1 3-3h4Zm32 0a3 3 0 0 1 3 3v4h-2v-4a1 1 0 0 0-.883-.993L45 15h-4v-2h4Zm-6 0v2H28v-2h11Zm-13 0v2H15v-2h11Z"
5622
6014
  })));
5623
6015
  };
5624
- var ColumnsIcon = SvgGroup;
6016
+ var GroupIcon = SvgGroup;
5625
6017
 
5626
6018
  var _path$7;
5627
6019
  function _extends$7() { _extends$7 = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends$7.apply(this, arguments); }
@@ -5674,14 +6066,14 @@ var SvgSpacer = function SvgSpacer(props) {
5674
6066
  xmlns: "http://www.w3.org/2000/svg",
5675
6067
  width: 54,
5676
6068
  height: 54,
5677
- fill: "none"
6069
+ fill: "currentcolor"
5678
6070
  }, props), _path$4 || (_path$4 = /*#__PURE__*/React__namespace.createElement("path", {
5679
- stroke: "#000",
6071
+ stroke: "currentcolor",
5680
6072
  strokeLinecap: "square",
5681
6073
  strokeWidth: 2,
5682
6074
  d: "M9 23h36M9 31h36"
5683
6075
  })), _path2$1 || (_path2$1 = /*#__PURE__*/React__namespace.createElement("path", {
5684
- stroke: "#000",
6076
+ stroke: "currentcolor",
5685
6077
  strokeLinecap: "round",
5686
6078
  strokeLinejoin: "round",
5687
6079
  strokeWidth: 2,
@@ -5759,8 +6151,9 @@ const iconsByType = type => {
5759
6151
  button: ButtonIcon,
5760
6152
  checkbox: CheckboxIcon,
5761
6153
  checklist: ChecklistIcon,
5762
- columns: ColumnsIcon,
6154
+ columns: GroupIcon,
5763
6155
  datetime: DatetimeIcon,
6156
+ group: GroupIcon,
5764
6157
  image: ImageIcon,
5765
6158
  number: NumberIcon,
5766
6159
  radio: RadioIcon,
@@ -5774,7 +6167,7 @@ const iconsByType = type => {
5774
6167
  }[type];
5775
6168
  };
5776
6169
 
5777
- const formFields = [Button, Checkbox, Checklist, Default, Image, Numberfield, Datetime, Radio, Select, Spacer, Taglist, Text, Textfield, Textarea];
6170
+ const formFields = [Button, Checkbox, Checklist, FormComponent$1, Group, Image, Numberfield, Datetime, Radio, Select, Spacer, Taglist, Text, Textfield, Textarea];
5778
6171
 
5779
6172
  class FormFields {
5780
6173
  constructor() {
@@ -5850,62 +6243,65 @@ var renderModule = {
5850
6243
  };
5851
6244
 
5852
6245
  var core = {
5853
- __depends__: [importModule, renderModule],
6246
+ __depends__: [renderModule],
5854
6247
  eventBus: ['type', EventBus],
6248
+ importer: ['type', Importer],
6249
+ fieldFactory: ['type', FieldFactory],
5855
6250
  formFieldRegistry: ['type', FormFieldRegistry],
6251
+ pathRegistry: ['type', PathRegistry],
5856
6252
  formLayouter: ['type', FormLayouter],
5857
6253
  validator: ['type', Validator]
5858
6254
  };
5859
6255
 
5860
- /**
5861
- * @typedef { import('./types').Injector } Injector
5862
- * @typedef { import('./types').Data } Data
5863
- * @typedef { import('./types').Errors } Errors
5864
- * @typedef { import('./types').Schema } Schema
5865
- * @typedef { import('./types').FormProperties } FormProperties
5866
- * @typedef { import('./types').FormProperty } FormProperty
5867
- * @typedef { import('./types').FormEvent } FormEvent
5868
- * @typedef { import('./types').FormOptions } FormOptions
5869
- *
5870
- * @typedef { {
5871
- * data: Data,
5872
- * initialData: Data,
5873
- * errors: Errors,
5874
- * properties: FormProperties,
5875
- * schema: Schema
5876
- * } } State
5877
- *
5878
- * @typedef { (type:FormEvent, priority:number, handler:Function) => void } OnEventWithPriority
5879
- * @typedef { (type:FormEvent, handler:Function) => void } OnEventWithOutPriority
5880
- * @typedef { OnEventWithPriority & OnEventWithOutPriority } OnEventType
6256
+ /**
6257
+ * @typedef { import('./types').Injector } Injector
6258
+ * @typedef { import('./types').Data } Data
6259
+ * @typedef { import('./types').Errors } Errors
6260
+ * @typedef { import('./types').Schema } Schema
6261
+ * @typedef { import('./types').FormProperties } FormProperties
6262
+ * @typedef { import('./types').FormProperty } FormProperty
6263
+ * @typedef { import('./types').FormEvent } FormEvent
6264
+ * @typedef { import('./types').FormOptions } FormOptions
6265
+ *
6266
+ * @typedef { {
6267
+ * data: Data,
6268
+ * initialData: Data,
6269
+ * errors: Errors,
6270
+ * properties: FormProperties,
6271
+ * schema: Schema
6272
+ * } } State
6273
+ *
6274
+ * @typedef { (type:FormEvent, priority:number, handler:Function) => void } OnEventWithPriority
6275
+ * @typedef { (type:FormEvent, handler:Function) => void } OnEventWithOutPriority
6276
+ * @typedef { OnEventWithPriority & OnEventWithOutPriority } OnEventType
5881
6277
  */
5882
6278
 
5883
6279
  const ids = new Ids([32, 36, 1]);
5884
6280
 
5885
- /**
5886
- * The form.
6281
+ /**
6282
+ * The form.
5887
6283
  */
5888
6284
  class Form {
5889
- /**
5890
- * @constructor
5891
- * @param {FormOptions} options
6285
+ /**
6286
+ * @constructor
6287
+ * @param {FormOptions} options
5892
6288
  */
5893
6289
  constructor(options = {}) {
5894
- /**
5895
- * @public
5896
- * @type {OnEventType}
6290
+ /**
6291
+ * @public
6292
+ * @type {OnEventType}
5897
6293
  */
5898
6294
  this.on = this._onEvent;
5899
6295
 
5900
- /**
5901
- * @public
5902
- * @type {String}
6296
+ /**
6297
+ * @public
6298
+ * @type {String}
5903
6299
  */
5904
6300
  this._id = ids.next();
5905
6301
 
5906
- /**
5907
- * @private
5908
- * @type {Element}
6302
+ /**
6303
+ * @private
6304
+ * @type {Element}
5909
6305
  */
5910
6306
  this._container = createFormContainer();
5911
6307
  const {
@@ -5914,9 +6310,9 @@ class Form {
5914
6310
  properties = {}
5915
6311
  } = options;
5916
6312
 
5917
- /**
5918
- * @private
5919
- * @type {State}
6313
+ /**
6314
+ * @private
6315
+ * @type {State}
5920
6316
  */
5921
6317
  this._state = {
5922
6318
  initialData: null,
@@ -5940,9 +6336,9 @@ class Form {
5940
6336
  this._emit('form.clear');
5941
6337
  }
5942
6338
 
5943
- /**
5944
- * Destroy the form, removing it from DOM,
5945
- * if attached.
6339
+ /**
6340
+ * Destroy the form, removing it from DOM,
6341
+ * if attached.
5946
6342
  */
5947
6343
  destroy() {
5948
6344
  // destroy form services
@@ -5953,13 +6349,13 @@ class Form {
5953
6349
  this._detach(false);
5954
6350
  }
5955
6351
 
5956
- /**
5957
- * Open a form schema with the given initial data.
5958
- *
5959
- * @param {Schema} schema
5960
- * @param {Data} [data]
5961
- *
5962
- * @return Promise<{ warnings: Array<any> }>
6352
+ /**
6353
+ * Open a form schema with the given initial data.
6354
+ *
6355
+ * @param {Schema} schema
6356
+ * @param {Data} [data]
6357
+ *
6358
+ * @return Promise<{ warnings: Array<any> }>
5963
6359
  */
5964
6360
  importSchema(schema, data = {}) {
5965
6361
  return new Promise((resolve, reject) => {
@@ -5967,9 +6363,9 @@ class Form {
5967
6363
  this.clear();
5968
6364
  const {
5969
6365
  schema: importedSchema,
5970
- data: initializedData,
5971
6366
  warnings
5972
- } = this.get('importer').importSchema(schema, data);
6367
+ } = this.get('importer').importSchema(schema);
6368
+ const initializedData = this._initializeFieldData(clone(data));
5973
6369
  this._setState({
5974
6370
  data: initializedData,
5975
6371
  errors: {},
@@ -5992,10 +6388,10 @@ class Form {
5992
6388
  });
5993
6389
  }
5994
6390
 
5995
- /**
5996
- * Submit the form, triggering all field validations.
5997
- *
5998
- * @returns { { data: Data, errors: Errors } }
6391
+ /**
6392
+ * Submit the form, triggering all field validations.
6393
+ *
6394
+ * @returns { { data: Data, errors: Errors } }
5999
6395
  */
6000
6396
  submit() {
6001
6397
  const {
@@ -6022,26 +6418,26 @@ class Form {
6022
6418
  });
6023
6419
  }
6024
6420
 
6025
- /**
6026
- * @returns {Errors}
6421
+ /**
6422
+ * @returns {Errors}
6027
6423
  */
6028
6424
  validate() {
6029
6425
  const formFieldRegistry = this.get('formFieldRegistry'),
6426
+ pathRegistry = this.get('pathRegistry'),
6030
6427
  validator = this.get('validator');
6031
6428
  const {
6032
6429
  data
6033
6430
  } = this._getState();
6034
6431
  const errors = formFieldRegistry.getAll().reduce((errors, field) => {
6035
6432
  const {
6036
- disabled,
6037
- _path
6433
+ disabled
6038
6434
  } = field;
6039
6435
  if (disabled) {
6040
6436
  return errors;
6041
6437
  }
6042
- const value = minDash.get(data, _path);
6438
+ const value = minDash.get(data, pathRegistry.getValuePath(field));
6043
6439
  const fieldErrors = validator.validateField(field, value);
6044
- return minDash.set(errors, [pathStringify(_path)], fieldErrors.length ? fieldErrors : undefined);
6440
+ return minDash.set(errors, [field.id], fieldErrors.length ? fieldErrors : undefined);
6045
6441
  }, /** @type {Errors} */{});
6046
6442
  this._setState({
6047
6443
  errors
@@ -6049,8 +6445,8 @@ class Form {
6049
6445
  return errors;
6050
6446
  }
6051
6447
 
6052
- /**
6053
- * @param {Element|string} parentNode
6448
+ /**
6449
+ * @param {Element|string} parentNode
6054
6450
  */
6055
6451
  attachTo(parentNode) {
6056
6452
  if (!parentNode) {
@@ -6068,10 +6464,10 @@ class Form {
6068
6464
  this._detach();
6069
6465
  }
6070
6466
 
6071
- /**
6072
- * @private
6073
- *
6074
- * @param {boolean} [emit]
6467
+ /**
6468
+ * @private
6469
+ *
6470
+ * @param {boolean} [emit]
6075
6471
  */
6076
6472
  _detach(emit = true) {
6077
6473
  const container = this._container,
@@ -6085,9 +6481,9 @@ class Form {
6085
6481
  parentNode.removeChild(container);
6086
6482
  }
6087
6483
 
6088
- /**
6089
- * @param {FormProperty} property
6090
- * @param {any} value
6484
+ /**
6485
+ * @param {FormProperty} property
6486
+ * @param {any} value
6091
6487
  */
6092
6488
  setProperty(property, value) {
6093
6489
  const properties = minDash.set(this._getState().properties, [property], value);
@@ -6096,21 +6492,21 @@ class Form {
6096
6492
  });
6097
6493
  }
6098
6494
 
6099
- /**
6100
- * @param {FormEvent} type
6101
- * @param {Function} handler
6495
+ /**
6496
+ * @param {FormEvent} type
6497
+ * @param {Function} handler
6102
6498
  */
6103
6499
  off(type, handler) {
6104
6500
  this.get('eventBus').off(type, handler);
6105
6501
  }
6106
6502
 
6107
- /**
6108
- * @private
6109
- *
6110
- * @param {FormOptions} options
6111
- * @param {Element} container
6112
- *
6113
- * @returns {Injector}
6503
+ /**
6504
+ * @private
6505
+ *
6506
+ * @param {FormOptions} options
6507
+ * @param {Element} container
6508
+ *
6509
+ * @returns {Injector}
6114
6510
  */
6115
6511
  _createInjector(options, container) {
6116
6512
  const {
@@ -6129,17 +6525,17 @@ class Form {
6129
6525
  }, core, ...modules, ...additionalModules]);
6130
6526
  }
6131
6527
 
6132
- /**
6133
- * @private
6528
+ /**
6529
+ * @private
6134
6530
  */
6135
6531
  _emit(type, data) {
6136
6532
  this.get('eventBus').fire(type, data);
6137
6533
  }
6138
6534
 
6139
- /**
6140
- * @internal
6141
- *
6142
- * @param { { add?: boolean, field: any, remove?: number, value?: any } } update
6535
+ /**
6536
+ * @internal
6537
+ *
6538
+ * @param { { add?: boolean, field: any, remove?: number, value?: any } } update
6143
6539
  */
6144
6540
  _update(update) {
6145
6541
  const {
@@ -6147,31 +6543,29 @@ class Form {
6147
6543
  value
6148
6544
  } = update;
6149
6545
  const {
6150
- _path
6151
- } = field;
6152
- let {
6153
6546
  data,
6154
6547
  errors
6155
6548
  } = this._getState();
6156
- const validator = this.get('validator');
6549
+ const validator = this.get('validator'),
6550
+ pathRegistry = this.get('pathRegistry');
6157
6551
  const fieldErrors = validator.validateField(field, value);
6158
- minDash.set(data, _path, value);
6159
- minDash.set(errors, [pathStringify(_path)], fieldErrors.length ? fieldErrors : undefined);
6552
+ minDash.set(data, pathRegistry.getValuePath(field), value);
6553
+ minDash.set(errors, [field.id], fieldErrors.length ? fieldErrors : undefined);
6160
6554
  this._setState({
6161
6555
  data: clone(data),
6162
6556
  errors: clone(errors)
6163
6557
  });
6164
6558
  }
6165
6559
 
6166
- /**
6167
- * @internal
6560
+ /**
6561
+ * @internal
6168
6562
  */
6169
6563
  _getState() {
6170
6564
  return this._state;
6171
6565
  }
6172
6566
 
6173
- /**
6174
- * @internal
6567
+ /**
6568
+ * @internal
6175
6569
  */
6176
6570
  _setState(state) {
6177
6571
  this._state = {
@@ -6181,56 +6575,96 @@ class Form {
6181
6575
  this._emit('changed', this._getState());
6182
6576
  }
6183
6577
 
6184
- /**
6185
- * @internal
6578
+ /**
6579
+ * @internal
6186
6580
  */
6187
6581
  _getModules() {
6188
6582
  return [ExpressionLanguageModule, MarkdownModule, ViewerCommandsModule];
6189
6583
  }
6190
6584
 
6191
- /**
6192
- * @internal
6585
+ /**
6586
+ * @internal
6193
6587
  */
6194
6588
  _onEvent(type, priority, handler) {
6195
6589
  this.get('eventBus').on(type, priority, handler);
6196
6590
  }
6197
6591
 
6198
- /**
6199
- * @internal
6592
+ /**
6593
+ * @internal
6200
6594
  */
6201
6595
  _getSubmitData() {
6202
- const formFieldRegistry = this.get('formFieldRegistry');
6596
+ const formFieldRegistry = this.get('formFieldRegistry'),
6597
+ pathRegistry = this.get('pathRegistry'),
6598
+ formFields = this.get('formFields');
6203
6599
  const formData = this._getState().data;
6204
6600
  const submitData = formFieldRegistry.getAll().reduce((previous, field) => {
6205
6601
  const {
6206
6602
  disabled,
6207
- _path
6603
+ type
6208
6604
  } = field;
6605
+ const {
6606
+ config: fieldConfig
6607
+ } = formFields.get(type);
6209
6608
 
6210
- // do not submit disabled form fields
6211
- if (disabled || !_path) {
6609
+ // do not submit disabled form fields or routing fields
6610
+ if (disabled || !fieldConfig.keyed) {
6212
6611
  return previous;
6213
6612
  }
6214
- const value = minDash.get(formData, _path);
6215
- return {
6216
- ...previous,
6217
- [_path[0]]: value
6218
- };
6613
+ const valuePath = pathRegistry.getValuePath(field);
6614
+ const value = minDash.get(formData, valuePath);
6615
+ return minDash.set(previous, valuePath, value);
6219
6616
  }, {});
6220
6617
  const filteredSubmitData = this._applyConditions(submitData, formData);
6221
6618
  return filteredSubmitData;
6222
6619
  }
6223
6620
 
6224
- /**
6225
- * @internal
6621
+ /**
6622
+ * @internal
6226
6623
  */
6227
6624
  _applyConditions(toFilter, data) {
6228
6625
  const conditionChecker = this.get('conditionChecker');
6229
6626
  return conditionChecker.applyConditions(toFilter, data);
6230
6627
  }
6628
+
6629
+ /**
6630
+ * @internal
6631
+ */
6632
+ _initializeFieldData(data) {
6633
+ const formFieldRegistry = this.get('formFieldRegistry'),
6634
+ formFields = this.get('formFields'),
6635
+ pathRegistry = this.get('pathRegistry');
6636
+ return formFieldRegistry.getAll().reduce((initializedData, formField) => {
6637
+ const {
6638
+ defaultValue,
6639
+ type
6640
+ } = formField;
6641
+
6642
+ // try to get value from data
6643
+ // if unavailable - try to get default value from form field
6644
+ // if unavailable - get empty value from form field
6645
+
6646
+ const valuePath = pathRegistry.getValuePath(formField);
6647
+ if (valuePath) {
6648
+ const {
6649
+ config: fieldConfig
6650
+ } = formFields.get(type);
6651
+ let valueData = minDash.get(data, valuePath);
6652
+ if (!minDash.isUndefined(valueData) && fieldConfig.sanitizeValue) {
6653
+ valueData = fieldConfig.sanitizeValue({
6654
+ formField,
6655
+ data,
6656
+ value: valueData
6657
+ });
6658
+ }
6659
+ const initializedFieldValue = !minDash.isUndefined(valueData) ? valueData : !minDash.isUndefined(defaultValue) ? defaultValue : fieldConfig.emptyValue;
6660
+ return minDash.set(initializedData, valuePath, initializedFieldValue);
6661
+ }
6662
+ return initializedData;
6663
+ }, data);
6664
+ }
6231
6665
  }
6232
6666
 
6233
- const schemaVersion = 10;
6667
+ const schemaVersion = 11;
6234
6668
 
6235
6669
  /**
6236
6670
  * @typedef { import('./types').CreateFormOptions } CreateFormOptions
@@ -6265,22 +6699,27 @@ exports.DATETIME_SUBTYPE_PATH = DATETIME_SUBTYPE_PATH;
6265
6699
  exports.DATE_DISALLOW_PAST_PATH = DATE_DISALLOW_PAST_PATH;
6266
6700
  exports.DATE_LABEL_PATH = DATE_LABEL_PATH;
6267
6701
  exports.Datetime = Datetime;
6268
- exports.Default = Default;
6702
+ exports.Default = FormComponent$1;
6269
6703
  exports.ExpressionLanguageModule = ExpressionLanguageModule;
6270
6704
  exports.FeelExpressionLanguage = FeelExpressionLanguage;
6271
6705
  exports.FeelersTemplating = FeelersTemplating;
6706
+ exports.FieldFactory = FieldFactory;
6272
6707
  exports.Form = Form;
6273
6708
  exports.FormComponent = FormComponent;
6274
6709
  exports.FormContext = FormContext$1;
6710
+ exports.FormField = FormField;
6275
6711
  exports.FormFieldRegistry = FormFieldRegistry;
6276
6712
  exports.FormFields = FormFields;
6277
6713
  exports.FormLayouter = FormLayouter;
6278
6714
  exports.FormRenderContext = FormRenderContext$1;
6715
+ exports.Group = Group;
6279
6716
  exports.Image = Image;
6717
+ exports.Importer = Importer;
6280
6718
  exports.MINUTES_IN_DAY = MINUTES_IN_DAY;
6281
6719
  exports.MarkdownModule = MarkdownModule;
6282
6720
  exports.MarkdownRenderer = MarkdownRenderer;
6283
6721
  exports.Numberfield = Numberfield;
6722
+ exports.PathRegistry = PathRegistry;
6284
6723
  exports.Radio = Radio;
6285
6724
  exports.Select = Select;
6286
6725
  exports.Spacer = Spacer;
@@ -6305,7 +6744,6 @@ exports.clone = clone;
6305
6744
  exports.createForm = createForm;
6306
6745
  exports.createFormContainer = createFormContainer;
6307
6746
  exports.createInjector = createInjector;
6308
- exports.findErrors = findErrors;
6309
6747
  exports.formFields = formFields;
6310
6748
  exports.generateIdForType = generateIdForType;
6311
6749
  exports.generateIndexForType = generateIndexForType;
@@ -6314,7 +6752,7 @@ exports.getValuesSource = getValuesSource;
6314
6752
  exports.iconsByType = iconsByType;
6315
6753
  exports.isRequired = isRequired;
6316
6754
  exports.pathParse = pathParse;
6317
- exports.pathStringify = pathStringify;
6318
6755
  exports.pathsEqual = pathsEqual;
6756
+ exports.runRecursively = runRecursively;
6319
6757
  exports.schemaVersion = schemaVersion;
6320
6758
  //# sourceMappingURL=index.cjs.map