@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.es.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import Ids from 'ids';
2
- import { isString, uniqueBy, isArray, get, set, isFunction, isNumber, bind, assign, isNil, groupBy, flatten, isUndefined, findIndex, isObject } from 'min-dash';
2
+ import { isString, get, set, isObject, values, uniqueBy, isArray, isFunction, isNumber, bind, assign, isNil, groupBy, flatten, findIndex, isUndefined } from 'min-dash';
3
3
  import Big from 'big.js';
4
4
  import { parseExpression, parseUnaryTests, evaluate, unaryTest } from 'feelin';
5
5
  import { evaluate as evaluate$1, parser, buildSimpleTree } from 'feelers';
6
6
  import classNames from 'classnames';
7
7
  import { jsx, jsxs, Fragment as Fragment$1 } from 'preact/jsx-runtime';
8
- import { useContext, useMemo, useEffect, useState, useRef, useCallback, useLayoutEffect } from 'preact/hooks';
8
+ import { useContext, useMemo, useEffect, useRef, useState, useCallback, useLayoutEffect } from 'preact/hooks';
9
9
  import { createContext, createElement, Fragment, render } from 'preact';
10
10
  import * as React from 'preact/compat';
11
11
  import { createPortal } from 'preact/compat';
@@ -387,14 +387,230 @@ class FeelersTemplating {
387
387
  }
388
388
  FeelersTemplating.$inject = [];
389
389
 
390
+ // config ///////////////////
391
+
392
+ const MINUTES_IN_DAY = 60 * 24;
393
+ const DATETIME_SUBTYPES = {
394
+ DATE: 'date',
395
+ TIME: 'time',
396
+ DATETIME: 'datetime'
397
+ };
398
+ const TIME_SERIALISING_FORMATS = {
399
+ UTC_OFFSET: 'utc_offset',
400
+ UTC_NORMALIZED: 'utc_normalized',
401
+ NO_TIMEZONE: 'no_timezone'
402
+ };
403
+ const DATETIME_SUBTYPES_LABELS = {
404
+ [DATETIME_SUBTYPES.DATE]: 'Date',
405
+ [DATETIME_SUBTYPES.TIME]: 'Time',
406
+ [DATETIME_SUBTYPES.DATETIME]: 'Date & Time'
407
+ };
408
+ const TIME_SERIALISINGFORMAT_LABELS = {
409
+ [TIME_SERIALISING_FORMATS.UTC_OFFSET]: 'UTC offset',
410
+ [TIME_SERIALISING_FORMATS.UTC_NORMALIZED]: 'UTC normalized',
411
+ [TIME_SERIALISING_FORMATS.NO_TIMEZONE]: 'No timezone'
412
+ };
413
+ const DATETIME_SUBTYPE_PATH = ['subtype'];
414
+ const DATE_LABEL_PATH = ['dateLabel'];
415
+ const DATE_DISALLOW_PAST_PATH = ['disallowPassedDates'];
416
+ const TIME_LABEL_PATH = ['timeLabel'];
417
+ const TIME_USE24H_PATH = ['use24h'];
418
+ const TIME_INTERVAL_PATH = ['timeInterval'];
419
+ const TIME_SERIALISING_FORMAT_PATH = ['timeSerializingFormat'];
420
+
421
+ // config ///////////////////
422
+
423
+ const VALUES_SOURCES = {
424
+ STATIC: 'static',
425
+ INPUT: 'input',
426
+ EXPRESSION: 'expression'
427
+ };
428
+ const VALUES_SOURCE_DEFAULT = VALUES_SOURCES.STATIC;
429
+ const VALUES_SOURCES_LABELS = {
430
+ [VALUES_SOURCES.STATIC]: 'Static',
431
+ [VALUES_SOURCES.INPUT]: 'Input data',
432
+ [VALUES_SOURCES.EXPRESSION]: 'Expression'
433
+ };
434
+ const VALUES_SOURCES_PATHS = {
435
+ [VALUES_SOURCES.STATIC]: ['values'],
436
+ [VALUES_SOURCES.INPUT]: ['valuesKey'],
437
+ [VALUES_SOURCES.EXPRESSION]: ['valuesExpression']
438
+ };
439
+ const VALUES_SOURCES_DEFAULTS = {
440
+ [VALUES_SOURCES.STATIC]: [{
441
+ label: 'Value',
442
+ value: 'value'
443
+ }],
444
+ [VALUES_SOURCES.INPUT]: '',
445
+ [VALUES_SOURCES.EXPRESSION]: '='
446
+ };
447
+
448
+ // helpers ///////////////////
449
+
450
+ function getValuesSource(field) {
451
+ for (const source of Object.values(VALUES_SOURCES)) {
452
+ if (get(field, VALUES_SOURCES_PATHS[source]) !== undefined) {
453
+ return source;
454
+ }
455
+ }
456
+ return VALUES_SOURCE_DEFAULT;
457
+ }
458
+
459
+ function createInjector(bootstrapModules) {
460
+ const injector = new Injector(bootstrapModules);
461
+ injector.init();
462
+ return injector;
463
+ }
464
+
465
+ /**
466
+ * @param {string?} prefix
467
+ *
468
+ * @returns Element
469
+ */
470
+ function createFormContainer(prefix = 'fjs') {
471
+ const container = document.createElement('div');
472
+ container.classList.add(`${prefix}-container`);
473
+ return container;
474
+ }
475
+
476
+ const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression'];
477
+ const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text'];
478
+ function isRequired(field) {
479
+ return field.required;
480
+ }
481
+ function pathParse(path) {
482
+ if (!path) {
483
+ return [];
484
+ }
485
+ return path.split('.').map(key => {
486
+ return isNaN(parseInt(key)) ? key : parseInt(key);
487
+ });
488
+ }
489
+ function pathsEqual(a, b) {
490
+ return a && b && a.length === b.length && a.every((value, index) => value === b[index]);
491
+ }
492
+ const indices = {};
493
+ function generateIndexForType(type) {
494
+ if (type in indices) {
495
+ indices[type]++;
496
+ } else {
497
+ indices[type] = 1;
498
+ }
499
+ return indices[type];
500
+ }
501
+ function generateIdForType(type) {
502
+ return `${type}${generateIndexForType(type)}`;
503
+ }
504
+
505
+ /**
506
+ * @template T
507
+ * @param {T} data
508
+ * @param {(this: any, key: string, value: any) => any} [replacer]
509
+ * @return {T}
510
+ */
511
+ function clone(data, replacer) {
512
+ return JSON.parse(JSON.stringify(data, replacer));
513
+ }
514
+
515
+ /**
516
+ * Parse the schema for input variables a form might make use of
517
+ *
518
+ * @param {any} schema
519
+ *
520
+ * @return {string[]}
521
+ */
522
+ function getSchemaVariables(schema, options = {}) {
523
+ const {
524
+ expressionLanguage = new FeelExpressionLanguage(null),
525
+ templating = new FeelersTemplating(),
526
+ inputs = true,
527
+ outputs = true
528
+ } = options;
529
+ if (!schema.components) {
530
+ return [];
531
+ }
532
+ const getAllComponents = node => {
533
+ const components = [];
534
+ if (node.components) {
535
+ node.components.forEach(component => {
536
+ components.push(component);
537
+ components.push(...getAllComponents(component));
538
+ });
539
+ }
540
+ return components;
541
+ };
542
+ const variables = getAllComponents(schema).reduce((variables, component) => {
543
+ const {
544
+ valuesKey
545
+ } = component;
546
+
547
+ // collect input-only variables
548
+ if (inputs) {
549
+ if (valuesKey) {
550
+ variables = [...variables, valuesKey];
551
+ }
552
+ EXPRESSION_PROPERTIES.forEach(prop => {
553
+ const property = get(component, prop.split('.'));
554
+ if (property && expressionLanguage.isExpression(property)) {
555
+ const expressionVariables = expressionLanguage.getVariableNames(property, {
556
+ type: 'expression'
557
+ });
558
+ variables = [...variables, ...expressionVariables];
559
+ }
560
+ });
561
+ TEMPLATE_PROPERTIES.forEach(prop => {
562
+ const property = get(component, prop.split('.'));
563
+ if (property && !expressionLanguage.isExpression(property) && templating.isTemplate(property)) {
564
+ const templateVariables = templating.getVariableNames(property);
565
+ variables = [...variables, ...templateVariables];
566
+ }
567
+ });
568
+ }
569
+ return variables.filter(variable => variable !== undefined || variable !== null);
570
+ }, []);
571
+ const getBindingVariables = node => {
572
+ const bindingVariable = [];
573
+
574
+ // c.f. https://github.com/bpmn-io/form-js/issues/778 @Skaiir to remove?
575
+ if (node.type === 'button') {
576
+ return [];
577
+ } else if (node.key) {
578
+ return [node.key.split('.')[0]];
579
+ } else if (node.path) {
580
+ return [node.path.split('.')[0]];
581
+ } else if (node.components) {
582
+ node.components.forEach(component => {
583
+ bindingVariable.push(...getBindingVariables(component));
584
+ });
585
+ }
586
+ return bindingVariable;
587
+ };
588
+
589
+ // collect binding variables
590
+ if (inputs || outputs) {
591
+ variables.push(...getBindingVariables(schema));
592
+ }
593
+
594
+ // remove duplicates
595
+ return Array.from(new Set(variables));
596
+ }
597
+ function runRecursively(formField, fn) {
598
+ const components = formField.components || [];
599
+ components.forEach((component, index) => {
600
+ runRecursively(component, fn);
601
+ });
602
+ fn(formField);
603
+ }
604
+
390
605
  /**
391
606
  * @typedef {object} Condition
392
607
  * @property {string} [hide]
393
608
  */
394
609
 
395
610
  class ConditionChecker {
396
- constructor(formFieldRegistry, eventBus) {
611
+ constructor(formFieldRegistry, pathRegistry, eventBus) {
397
612
  this._formFieldRegistry = formFieldRegistry;
613
+ this._pathRegistry = pathRegistry;
398
614
  this._eventBus = eventBus;
399
615
  }
400
616
 
@@ -405,19 +621,27 @@ class ConditionChecker {
405
621
  * @param {Object<string, any>} data
406
622
  */
407
623
  applyConditions(properties, data = {}) {
408
- const conditions = this._getConditions();
409
- const newProperties = {
410
- ...properties
411
- };
412
- for (const {
413
- key,
414
- condition
415
- } of conditions) {
416
- const shouldRemove = this._checkHideCondition(condition, data);
417
- if (shouldRemove) {
418
- delete newProperties[key];
419
- }
624
+ const newProperties = clone(properties);
625
+ const form = this._formFieldRegistry.getAll().find(field => field.type === 'default');
626
+ if (!form) {
627
+ throw new Error('form field registry has no form');
420
628
  }
629
+ this._pathRegistry.executeRecursivelyOnFields(form, ({
630
+ field,
631
+ isClosed,
632
+ context
633
+ }) => {
634
+ const {
635
+ conditional: condition
636
+ } = field;
637
+ context.isHidden = context.isHidden || condition && this._checkHideCondition(condition, data);
638
+
639
+ // only clear the leaf nodes, as groups may both point to the same path
640
+ if (context.isHidden && isClosed) {
641
+ const valuePath = this._pathRegistry.getValuePath(field);
642
+ this._clearObjectValueRecursively(valuePath, newProperties);
643
+ }
644
+ });
421
645
  return newProperties;
422
646
  }
423
647
 
@@ -462,24 +686,18 @@ class ConditionChecker {
462
686
  const result = this.check(condition.hide, data);
463
687
  return result === true;
464
688
  }
465
- _getConditions() {
466
- const formFields = this._formFieldRegistry.getAll();
467
- return formFields.reduce((conditions, formField) => {
468
- const {
469
- key,
470
- conditional: condition
471
- } = formField;
472
- if (key && condition) {
473
- return [...conditions, {
474
- key,
475
- condition
476
- }];
477
- }
478
- return conditions;
479
- }, []);
689
+ _clearObjectValueRecursively(valuePath, obj) {
690
+ const workingValuePath = [...valuePath];
691
+ let recurse = false;
692
+ do {
693
+ set(obj, workingValuePath, undefined);
694
+ workingValuePath.pop();
695
+ const parentObject = get(obj, workingValuePath);
696
+ recurse = isObject(parentObject) && !values(parentObject).length && !!workingValuePath.length;
697
+ } while (recurse);
480
698
  }
481
699
  }
482
- ConditionChecker.$inject = ['formFieldRegistry', 'eventBus'];
700
+ ConditionChecker.$inject = ['formFieldRegistry', 'pathRegistry', 'eventBus'];
483
701
 
484
702
  var ExpressionLanguageModule = {
485
703
  __init__: ['expressionLanguage', 'templating', 'conditionChecker'],
@@ -947,239 +1165,42 @@ CommandStack.prototype._popAction = function () {
947
1165
  execution.trigger = null;
948
1166
  }
949
1167
  };
950
- CommandStack.prototype._markDirty = function (elements) {
951
- const execution = this._currentExecution;
952
- if (!elements) {
953
- return;
954
- }
955
- elements = isArray(elements) ? elements : [elements];
956
- execution.dirty = execution.dirty.concat(elements);
957
- };
958
- CommandStack.prototype._executedAction = function (action, redo) {
959
- const stackIdx = ++this._stackIdx;
960
- if (!redo) {
961
- this._stack.splice(stackIdx, this._stack.length, action);
962
- }
963
- };
964
- CommandStack.prototype._revertedAction = function (action) {
965
- this._stackIdx--;
966
- };
967
- CommandStack.prototype._getHandler = function (command) {
968
- return this._handlerMap[command];
969
- };
970
- CommandStack.prototype._setHandler = function (command, handler) {
971
- if (!command || !handler) {
972
- throw new Error('command and handler required');
973
- }
974
- if (this._handlerMap[command]) {
975
- throw new Error('overriding handler for command <' + command + '>');
976
- }
977
- this._handlerMap[command] = handler;
978
- };
979
-
980
- /**
981
- * @type { import('didi').ModuleDeclaration }
982
- */
983
- var commandModule = {
984
- commandStack: ['type', CommandStack]
985
- };
986
-
987
- // config ///////////////////
988
-
989
- const MINUTES_IN_DAY = 60 * 24;
990
- const DATETIME_SUBTYPES = {
991
- DATE: 'date',
992
- TIME: 'time',
993
- DATETIME: 'datetime'
994
- };
995
- const TIME_SERIALISING_FORMATS = {
996
- UTC_OFFSET: 'utc_offset',
997
- UTC_NORMALIZED: 'utc_normalized',
998
- NO_TIMEZONE: 'no_timezone'
999
- };
1000
- const DATETIME_SUBTYPES_LABELS = {
1001
- [DATETIME_SUBTYPES.DATE]: 'Date',
1002
- [DATETIME_SUBTYPES.TIME]: 'Time',
1003
- [DATETIME_SUBTYPES.DATETIME]: 'Date & Time'
1004
- };
1005
- const TIME_SERIALISINGFORMAT_LABELS = {
1006
- [TIME_SERIALISING_FORMATS.UTC_OFFSET]: 'UTC offset',
1007
- [TIME_SERIALISING_FORMATS.UTC_NORMALIZED]: 'UTC normalized',
1008
- [TIME_SERIALISING_FORMATS.NO_TIMEZONE]: 'No timezone'
1009
- };
1010
- const DATETIME_SUBTYPE_PATH = ['subtype'];
1011
- const DATE_LABEL_PATH = ['dateLabel'];
1012
- const DATE_DISALLOW_PAST_PATH = ['disallowPassedDates'];
1013
- const TIME_LABEL_PATH = ['timeLabel'];
1014
- const TIME_USE24H_PATH = ['use24h'];
1015
- const TIME_INTERVAL_PATH = ['timeInterval'];
1016
- const TIME_SERIALISING_FORMAT_PATH = ['timeSerializingFormat'];
1017
-
1018
- // config ///////////////////
1019
-
1020
- const VALUES_SOURCES = {
1021
- STATIC: 'static',
1022
- INPUT: 'input',
1023
- EXPRESSION: 'expression'
1024
- };
1025
- const VALUES_SOURCE_DEFAULT = VALUES_SOURCES.STATIC;
1026
- const VALUES_SOURCES_LABELS = {
1027
- [VALUES_SOURCES.STATIC]: 'Static',
1028
- [VALUES_SOURCES.INPUT]: 'Input data',
1029
- [VALUES_SOURCES.EXPRESSION]: 'Expression'
1030
- };
1031
- const VALUES_SOURCES_PATHS = {
1032
- [VALUES_SOURCES.STATIC]: ['values'],
1033
- [VALUES_SOURCES.INPUT]: ['valuesKey'],
1034
- [VALUES_SOURCES.EXPRESSION]: ['valuesExpression']
1035
- };
1036
- const VALUES_SOURCES_DEFAULTS = {
1037
- [VALUES_SOURCES.STATIC]: [{
1038
- label: 'Value',
1039
- value: 'value'
1040
- }],
1041
- [VALUES_SOURCES.INPUT]: '',
1042
- [VALUES_SOURCES.EXPRESSION]: '='
1043
- };
1044
-
1045
- // helpers ///////////////////
1046
-
1047
- function getValuesSource(field) {
1048
- for (const source of Object.values(VALUES_SOURCES)) {
1049
- if (get(field, VALUES_SOURCES_PATHS[source]) !== undefined) {
1050
- return source;
1051
- }
1052
- }
1053
- return VALUES_SOURCE_DEFAULT;
1054
- }
1055
-
1056
- function createInjector(bootstrapModules) {
1057
- const injector = new Injector(bootstrapModules);
1058
- injector.init();
1059
- return injector;
1060
- }
1061
-
1062
- /**
1063
- * @param {string?} prefix
1064
- *
1065
- * @returns Element
1066
- */
1067
- function createFormContainer(prefix = 'fjs') {
1068
- const container = document.createElement('div');
1069
- container.classList.add(`${prefix}-container`);
1070
- return container;
1071
- }
1072
-
1073
- const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression'];
1074
- const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text'];
1075
- function findErrors(errors, path) {
1076
- return errors[pathStringify(path)];
1077
- }
1078
- function isRequired(field) {
1079
- return field.required;
1080
- }
1081
- function pathParse(path) {
1082
- if (!path) {
1083
- return [];
1084
- }
1085
- return path.split('.').map(key => {
1086
- return isNaN(parseInt(key)) ? key : parseInt(key);
1087
- });
1088
- }
1089
- function pathsEqual(a, b) {
1090
- return a && b && a.length === b.length && a.every((value, index) => value === b[index]);
1091
- }
1092
- function pathStringify(path) {
1093
- if (!path) {
1094
- return '';
1095
- }
1096
- return path.join('.');
1097
- }
1098
- const indices = {};
1099
- function generateIndexForType(type) {
1100
- if (type in indices) {
1101
- indices[type]++;
1102
- } else {
1103
- indices[type] = 1;
1104
- }
1105
- return indices[type];
1106
- }
1107
- function generateIdForType(type) {
1108
- return `${type}${generateIndexForType(type)}`;
1109
- }
1110
-
1111
- /**
1112
- * @template T
1113
- * @param {T} data
1114
- * @param {(this: any, key: string, value: any) => any} [replacer]
1115
- * @return {T}
1116
- */
1117
- function clone(data, replacer) {
1118
- return JSON.parse(JSON.stringify(data, replacer));
1119
- }
1120
-
1121
- /**
1122
- * Parse the schema for input variables a form might make use of
1123
- *
1124
- * @param {any} schema
1125
- *
1126
- * @return {string[]}
1127
- */
1128
- function getSchemaVariables(schema, options = {}) {
1129
- const {
1130
- expressionLanguage = new FeelExpressionLanguage(null),
1131
- templating = new FeelersTemplating(),
1132
- inputs = true,
1133
- outputs = true
1134
- } = options;
1135
- if (!schema.components) {
1136
- return [];
1137
- }
1138
- const variables = schema.components.reduce((variables, component) => {
1139
- const {
1140
- key,
1141
- valuesKey,
1142
- type
1143
- } = component;
1144
- if (['button'].includes(type)) {
1145
- return variables;
1146
- }
1147
-
1148
- // collect bi-directional variables
1149
- if (inputs || outputs) {
1150
- if (key) {
1151
- variables = [...variables, key];
1152
- }
1153
- }
1154
-
1155
- // collect input-only variables
1156
- if (inputs) {
1157
- if (valuesKey) {
1158
- variables = [...variables, valuesKey];
1159
- }
1160
- EXPRESSION_PROPERTIES.forEach(prop => {
1161
- const property = get(component, prop.split('.'));
1162
- if (property && expressionLanguage.isExpression(property)) {
1163
- const expressionVariables = expressionLanguage.getVariableNames(property, {
1164
- type: 'expression'
1165
- });
1166
- variables = [...variables, ...expressionVariables];
1167
- }
1168
- });
1169
- TEMPLATE_PROPERTIES.forEach(prop => {
1170
- const property = get(component, prop.split('.'));
1171
- if (property && !expressionLanguage.isExpression(property) && templating.isTemplate(property)) {
1172
- const templateVariables = templating.getVariableNames(property);
1173
- variables = [...variables, ...templateVariables];
1174
- }
1175
- });
1176
- }
1177
- return variables.filter(variable => variable !== undefined || variable !== null);
1178
- }, []);
1168
+ CommandStack.prototype._markDirty = function (elements) {
1169
+ const execution = this._currentExecution;
1170
+ if (!elements) {
1171
+ return;
1172
+ }
1173
+ elements = isArray(elements) ? elements : [elements];
1174
+ execution.dirty = execution.dirty.concat(elements);
1175
+ };
1176
+ CommandStack.prototype._executedAction = function (action, redo) {
1177
+ const stackIdx = ++this._stackIdx;
1178
+ if (!redo) {
1179
+ this._stack.splice(stackIdx, this._stack.length, action);
1180
+ }
1181
+ };
1182
+ CommandStack.prototype._revertedAction = function (action) {
1183
+ this._stackIdx--;
1184
+ };
1185
+ CommandStack.prototype._getHandler = function (command) {
1186
+ return this._handlerMap[command];
1187
+ };
1188
+ CommandStack.prototype._setHandler = function (command, handler) {
1189
+ if (!command || !handler) {
1190
+ throw new Error('command and handler required');
1191
+ }
1192
+ if (this._handlerMap[command]) {
1193
+ throw new Error('overriding handler for command <' + command + '>');
1194
+ }
1195
+ this._handlerMap[command] = handler;
1196
+ };
1179
1197
 
1180
- // remove duplicates
1181
- return Array.from(new Set(variables));
1182
- }
1198
+ /**
1199
+ * @type { import('didi').ModuleDeclaration }
1200
+ */
1201
+ var commandModule = {
1202
+ commandStack: ['type', CommandStack]
1203
+ };
1183
1204
 
1184
1205
  class UpdateFieldValidationHandler {
1185
1206
  constructor(form, validator) {
@@ -1191,15 +1212,12 @@ class UpdateFieldValidationHandler {
1191
1212
  field,
1192
1213
  value
1193
1214
  } = context;
1194
- const {
1195
- _path
1196
- } = field;
1197
1215
  const {
1198
1216
  errors
1199
1217
  } = this._form._getState();
1200
1218
  context.oldErrors = clone(errors);
1201
1219
  const fieldErrors = this._validator.validateField(field, value);
1202
- const updatedErrors = set(errors, [pathStringify(_path)], fieldErrors.length ? fieldErrors : undefined);
1220
+ const updatedErrors = set(errors, [field.id], fieldErrors.length ? fieldErrors : undefined);
1203
1221
  this._form._setState({
1204
1222
  errors: updatedErrors
1205
1223
  });
@@ -1882,54 +1900,397 @@ function evaluateFEELValues(validate, expressionLanguage, conditionChecker, form
1882
1900
  return evaluatedValidate;
1883
1901
  }
1884
1902
 
1885
- class FormFieldRegistry {
1886
- constructor(eventBus) {
1887
- this._eventBus = eventBus;
1888
- this._formFields = {};
1889
- eventBus.on('form.clear', () => this.clear());
1890
- this._ids = new Ids([32, 36, 1]);
1891
- this._keys = new Ids([32, 36, 1]);
1903
+ class Importer {
1904
+ /**
1905
+ * @constructor
1906
+ * @param { import('./FormFieldRegistry').default } formFieldRegistry
1907
+ * @param { import('./PathRegistry').default } pathRegistry
1908
+ * @param { import('./FieldFactory').default } fieldFactory
1909
+ * @param { import('./FormLayouter').default } formLayouter
1910
+ */
1911
+ constructor(formFieldRegistry, pathRegistry, fieldFactory, formLayouter) {
1912
+ this._formFieldRegistry = formFieldRegistry;
1913
+ this._pathRegistry = pathRegistry;
1914
+ this._fieldFactory = fieldFactory;
1915
+ this._formLayouter = formLayouter;
1892
1916
  }
1893
- add(formField) {
1917
+
1918
+ /**
1919
+ * Import schema creating rows, fields, attaching additional
1920
+ * information to each field and adding fields to the
1921
+ * field registry.
1922
+ *
1923
+ * Additional information attached:
1924
+ *
1925
+ * * `id` (unless present)
1926
+ * * `_parent`
1927
+ * * `_path`
1928
+ *
1929
+ * @param {any} schema
1930
+ *
1931
+ * @typedef {{ warnings: Error[], schema: any }} ImportResult
1932
+ * @returns {ImportResult}
1933
+ */
1934
+ importSchema(schema) {
1935
+ // TODO: Add warnings
1936
+ const warnings = [];
1937
+ try {
1938
+ this._cleanup();
1939
+ const importedSchema = this.importFormField(clone(schema));
1940
+ this._formLayouter.calculateLayout(clone(importedSchema));
1941
+ return {
1942
+ schema: importedSchema,
1943
+ warnings
1944
+ };
1945
+ } catch (err) {
1946
+ this._cleanup();
1947
+ err.warnings = warnings;
1948
+ throw err;
1949
+ }
1950
+ }
1951
+ _cleanup() {
1952
+ this._formLayouter.clear();
1953
+ this._formFieldRegistry.clear();
1954
+ this._pathRegistry.clear();
1955
+ }
1956
+
1957
+ /**
1958
+ * @param {{[x: string]: any}} fieldAttrs
1959
+ * @param {String} [parentId]
1960
+ * @param {number} [index]
1961
+ *
1962
+ * @return {any} field
1963
+ */
1964
+ importFormField(fieldAttrs, parentId, index) {
1894
1965
  const {
1895
- id
1896
- } = formField;
1897
- if (this._formFields[id]) {
1898
- throw new Error(`form field with ID ${id} already exists`);
1966
+ components
1967
+ } = fieldAttrs;
1968
+ let parent, path;
1969
+ if (parentId) {
1970
+ parent = this._formFieldRegistry.get(parentId);
1899
1971
  }
1900
- this._eventBus.fire('formField.add', {
1901
- formField
1972
+
1973
+ // set form field path
1974
+ path = parent ? [...parent._path, 'components', index] : [];
1975
+ const field = this._fieldFactory.create({
1976
+ ...fieldAttrs,
1977
+ _path: path,
1978
+ _parent: parentId
1979
+ }, false);
1980
+ this._formFieldRegistry.add(field);
1981
+ if (components) {
1982
+ field.components = this.importFormFields(components, field.id);
1983
+ }
1984
+ return field;
1985
+ }
1986
+
1987
+ /**
1988
+ * @param {Array<any>} components
1989
+ * @param {string} parentId
1990
+ *
1991
+ * @return {Array<any>} imported components
1992
+ */
1993
+ importFormFields(components, parentId) {
1994
+ return components.map((component, index) => {
1995
+ return this.importFormField(component, parentId, index);
1902
1996
  });
1903
- this._formFields[id] = formField;
1904
1997
  }
1905
- remove(formField) {
1998
+ }
1999
+ Importer.$inject = ['formFieldRegistry', 'pathRegistry', 'fieldFactory', 'formLayouter'];
2000
+
2001
+ class FieldFactory {
2002
+ /**
2003
+ * @constructor
2004
+ *
2005
+ * @param formFieldRegistry
2006
+ * @param formFields
2007
+ */
2008
+ constructor(formFieldRegistry, pathRegistry, formFields) {
2009
+ this._formFieldRegistry = formFieldRegistry;
2010
+ this._pathRegistry = pathRegistry;
2011
+ this._formFields = formFields;
2012
+ }
2013
+ create(attrs, applyDefaults = true) {
1906
2014
  const {
1907
- id
1908
- } = formField;
1909
- if (!this._formFields[id]) {
1910
- return;
2015
+ id,
2016
+ type,
2017
+ key,
2018
+ path,
2019
+ _parent
2020
+ } = attrs;
2021
+ const fieldDefinition = this._formFields.get(type);
2022
+ if (!fieldDefinition) {
2023
+ throw new Error(`form field of type <${type}> not supported`);
1911
2024
  }
1912
- this._eventBus.fire('formField.remove', {
1913
- formField
2025
+ const {
2026
+ config
2027
+ } = fieldDefinition;
2028
+ if (!config) {
2029
+ throw new Error(`form field of type <${type}> has no config`);
2030
+ }
2031
+ if (id && this._formFieldRegistry._ids.assigned(id)) {
2032
+ throw new Error(`form field with id <${id}> already exists`);
2033
+ }
2034
+
2035
+ // ensure that we can claim the path
2036
+
2037
+ const parent = _parent && this._formFieldRegistry.get(_parent);
2038
+ const parentPath = parent && this._pathRegistry.getValuePath(parent) || [];
2039
+ if (config.keyed && key && !this._pathRegistry.canClaimPath([...parentPath, ...key.split('.')], true)) {
2040
+ throw new Error(`binding path '${[...parentPath, key].join('.')}' is already claimed`);
2041
+ }
2042
+ if (config.pathed && path && !this._pathRegistry.canClaimPath([...parentPath, ...path.split('.')], false)) {
2043
+ throw new Error(`binding path '${[...parentPath, ...path.split('.')].join('.')}' is already claimed`);
2044
+ }
2045
+ const labelAttrs = applyDefaults && config.label ? {
2046
+ label: config.label
2047
+ } : {};
2048
+ const field = config.create({
2049
+ ...labelAttrs,
2050
+ ...attrs
1914
2051
  });
1915
- delete this._formFields[id];
2052
+ this._ensureId(field);
2053
+ if (config.keyed) {
2054
+ this._ensureKey(field);
2055
+ }
2056
+ if (config.pathed && path) {
2057
+ this._pathRegistry.claimPath(this._pathRegistry.getValuePath(field), false);
2058
+ }
2059
+ return field;
1916
2060
  }
1917
- get(id) {
1918
- return this._formFields[id];
2061
+ _ensureId(field) {
2062
+ if (field.id) {
2063
+ this._formFieldRegistry._ids.claim(field.id, field);
2064
+ return;
2065
+ }
2066
+ let prefix = 'Field';
2067
+ if (field.type === 'default') {
2068
+ prefix = 'Form';
2069
+ }
2070
+ field.id = this._formFieldRegistry._ids.nextPrefixed(`${prefix}_`, field);
1919
2071
  }
1920
- getAll() {
1921
- return Object.values(this._formFields);
2072
+ _ensureKey(field) {
2073
+ if (!field.key) {
2074
+ let random;
2075
+ const parent = this._formFieldRegistry.get(field._parent);
2076
+
2077
+ // ensure key uniqueness at level
2078
+ do {
2079
+ random = Math.random().toString(36).substring(7);
2080
+ } while (parent && parent.components.some(child => child.key === random));
2081
+ field.key = `${field.type}_${random}`;
2082
+ }
2083
+ this._pathRegistry.claimPath(this._pathRegistry.getValuePath(field), true);
1922
2084
  }
1923
- forEach(callback) {
1924
- this.getAll().forEach(formField => callback(formField));
2085
+ }
2086
+ FieldFactory.$inject = ['formFieldRegistry', 'pathRegistry', 'formFields'];
2087
+
2088
+ /**
2089
+ * The PathRegistry class manages a hierarchical structure of paths associated with form fields.
2090
+ * It enables claiming, unclaiming, and validating paths within this structure.
2091
+ *
2092
+ * Example Tree Structure:
2093
+ *
2094
+ * [
2095
+ * {
2096
+ * segment: 'root',
2097
+ * claimCount: 1,
2098
+ * children: [
2099
+ * {
2100
+ * segment: 'child1',
2101
+ * claimCount: 2,
2102
+ * children: null // A leaf node (closed path)
2103
+ * },
2104
+ * {
2105
+ * segment: 'child2',
2106
+ * claimCount: 1,
2107
+ * children: [
2108
+ * {
2109
+ * segment: 'subChild1',
2110
+ * claimCount: 1,
2111
+ * children: [] // An open node (open path)
2112
+ * }
2113
+ * ]
2114
+ * }
2115
+ * ]
2116
+ * }
2117
+ * ]
2118
+ */
2119
+ class PathRegistry {
2120
+ constructor(formFieldRegistry, formFields) {
2121
+ this._formFieldRegistry = formFieldRegistry;
2122
+ this._formFields = formFields;
2123
+ this._dataPaths = [];
2124
+ }
2125
+ canClaimPath(path, closed = false) {
2126
+ let node = {
2127
+ children: this._dataPaths
2128
+ };
2129
+ for (const segment of path) {
2130
+ node = _getNextSegment(node, segment);
2131
+
2132
+ // if no node at that path, we can claim it no matter what
2133
+ if (!node) {
2134
+ return true;
2135
+ }
2136
+
2137
+ // if we reach a leaf node, definitely not claimable
2138
+ if (node.children === null) {
2139
+ return false;
2140
+ }
2141
+ }
2142
+
2143
+ // if after all segments we reach a node with children, we can claim it only openly
2144
+ return !closed;
2145
+ }
2146
+ claimPath(path, closed = false) {
2147
+ if (!this.canClaimPath(path, closed)) {
2148
+ throw new Error(`cannot claim path '${path.join('.')}'`);
2149
+ }
2150
+ let node = {
2151
+ children: this._dataPaths
2152
+ };
2153
+ for (const segment of path) {
2154
+ let child = _getNextSegment(node, segment);
2155
+ if (!child) {
2156
+ child = {
2157
+ segment,
2158
+ claimCount: 1,
2159
+ children: []
2160
+ };
2161
+ node.children.push(child);
2162
+ } else {
2163
+ child.claimCount++;
2164
+ }
2165
+ node = child;
2166
+ }
2167
+ if (closed) {
2168
+ node.children = null;
2169
+ }
2170
+ }
2171
+ unclaimPath(path) {
2172
+ // verification Pass
2173
+ let node = {
2174
+ children: this._dataPaths
2175
+ };
2176
+ for (const segment of path) {
2177
+ const child = _getNextSegment(node, segment);
2178
+ if (!child) {
2179
+ throw new Error(`no open path found for '${path.join('.')}'`);
2180
+ }
2181
+ node = child;
2182
+ }
2183
+
2184
+ // mutation Pass
2185
+ node = {
2186
+ children: this._dataPaths
2187
+ };
2188
+ for (const segment of path) {
2189
+ const child = _getNextSegment(node, segment);
2190
+ child.claimCount--;
2191
+ if (child.claimCount === 0) {
2192
+ node.children.splice(node.children.indexOf(child), 1);
2193
+ break; // Abort early if claimCount reaches zero
2194
+ }
2195
+
2196
+ node = child;
2197
+ }
2198
+ }
2199
+
2200
+ /**
2201
+ * Applies a function (fn) recursively on a given field and its children.
2202
+ *
2203
+ * - `field`: Starting field object.
2204
+ * - `fn`: Function to apply.
2205
+ * - `context`: Optional object for passing data between calls.
2206
+ *
2207
+ * Stops early if `fn` returns `false`. Useful for traversing the form field tree.
2208
+ *
2209
+ * @returns {boolean} Success status based on function execution.
2210
+ */
2211
+ executeRecursivelyOnFields(field, fn, context = {}) {
2212
+ let result = true;
2213
+ const formFieldConfig = this._formFields.get(field.type).config;
2214
+ if (formFieldConfig.keyed) {
2215
+ const callResult = fn({
2216
+ field,
2217
+ isClosed: true,
2218
+ context
2219
+ });
2220
+ return result && callResult;
2221
+ } else if (formFieldConfig.pathed) {
2222
+ const callResult = fn({
2223
+ field,
2224
+ isClosed: false,
2225
+ context
2226
+ });
2227
+ result = result && callResult;
2228
+ }
2229
+ if (field.components) {
2230
+ for (const child of field.components) {
2231
+ const callResult = this.executeRecursivelyOnFields(child, fn, clone(context));
2232
+ result = result && callResult;
2233
+
2234
+ // only stop executing if false is specifically returned, not if undefined
2235
+ if (result === false) {
2236
+ return result;
2237
+ }
2238
+ }
2239
+ }
2240
+ return result;
2241
+ }
2242
+
2243
+ /**
2244
+ * Generates an array representing the binding path to an underlying data object for a form field.
2245
+ *
2246
+ * @param {Object} field - The field object with properties: `key`, `path`, `id`, and optionally `_parent`.
2247
+ * @param {Object} [options={}] - Configuration options.
2248
+ * @param {Object} [options.replacements={}] - A map of field IDs to alternative path arrays.
2249
+ * @param {Object} [options.cutoffNode] - The ID of the parent field at which to stop generating the path.
2250
+ *
2251
+ * @returns {(Array<string>|undefined)} An array of strings representing the binding path, or undefined if not determinable.
2252
+ */
2253
+ getValuePath(field, options = {}) {
2254
+ const {
2255
+ replacements = {},
2256
+ cutoffNode = null
2257
+ } = options;
2258
+ let localValuePath = [];
2259
+ const hasReplacement = Object.prototype.hasOwnProperty.call(replacements, field.id);
2260
+ const formFieldConfig = this._formFields.get(field.type).config;
2261
+ if (hasReplacement) {
2262
+ const replacement = replacements[field.id];
2263
+ if (replacement === null || replacement === undefined || replacement === '') {
2264
+ localValuePath = [];
2265
+ } else if (typeof replacement === 'string') {
2266
+ localValuePath = replacement.split('.');
2267
+ } else if (Array.isArray(replacement)) {
2268
+ localValuePath = replacement;
2269
+ } else {
2270
+ throw new Error(`replacements for field ${field.id} must be a string, array or null/undefined`);
2271
+ }
2272
+ } else if (formFieldConfig.keyed) {
2273
+ localValuePath = field.key.split('.');
2274
+ } else if (formFieldConfig.pathed && field.path) {
2275
+ localValuePath = field.path.split('.');
2276
+ }
2277
+ if (field._parent && field._parent !== cutoffNode) {
2278
+ const parent = this._formFieldRegistry.get(field._parent);
2279
+ return [...(this.getValuePath(parent, options) || []), ...localValuePath];
2280
+ }
2281
+ return localValuePath;
1925
2282
  }
1926
2283
  clear() {
1927
- this._formFields = {};
1928
- this._ids.clear();
1929
- this._keys.clear();
2284
+ this._dataPaths = [];
1930
2285
  }
1931
2286
  }
1932
- FormFieldRegistry.$inject = ['eventBus'];
2287
+ const _getNextSegment = (node, segment) => {
2288
+ if (isArray(node.children)) {
2289
+ return node.children.find(node => node.segment === segment) || null;
2290
+ }
2291
+ return null;
2292
+ };
2293
+ PathRegistry.$inject = ['formFieldRegistry', 'formFields'];
1933
2294
 
1934
2295
  /**
1935
2296
  * @typedef { { id: String, components: Array<String> } } FormRow
@@ -2021,7 +2382,7 @@ class FormLayouter {
2021
2382
  type,
2022
2383
  components
2023
2384
  } = formField;
2024
- if (type !== 'default' || !components) {
2385
+ if (type !== 'default' && type !== 'group' || !components) {
2025
2386
  return;
2026
2387
  }
2027
2388
 
@@ -2076,147 +2437,52 @@ function allRows(formRows) {
2076
2437
  return flatten(formRows.map(c => c.rows));
2077
2438
  }
2078
2439
 
2079
- class Importer {
2080
- /**
2081
- * @constructor
2082
- * @param { import('../core').FormFieldRegistry } formFieldRegistry
2083
- * @param { import('../render/FormFields').default } formFields
2084
- * @param { import('../core').FormLayouter } formLayouter
2085
- */
2086
- constructor(formFieldRegistry, formFields, formLayouter) {
2087
- this._formFieldRegistry = formFieldRegistry;
2088
- this._formFields = formFields;
2089
- this._formLayouter = formLayouter;
2090
- }
2091
-
2092
- /**
2093
- * Import schema adding `id`, `_parent` and `_path`
2094
- * information to each field and adding it to the
2095
- * form field registry.
2096
- *
2097
- * @param {any} schema
2098
- * @param {any} [data]
2099
- *
2100
- * @return { { warnings: Array<any>, schema: any, data: any } }
2101
- */
2102
- importSchema(schema, data = {}) {
2103
- // TODO: Add warnings - https://github.com/bpmn-io/form-js/issues/289
2104
- const warnings = [];
2105
- try {
2106
- this._formLayouter.clear();
2107
- const importedSchema = this.importFormField(clone(schema)),
2108
- initializedData = this.initializeFieldValues(clone(data));
2109
- this._formLayouter.calculateLayout(clone(importedSchema));
2110
- return {
2111
- warnings,
2112
- schema: importedSchema,
2113
- data: initializedData
2114
- };
2115
- } catch (err) {
2116
- err.warnings = warnings;
2117
- throw err;
2118
- }
2440
+ class FormFieldRegistry {
2441
+ constructor(eventBus) {
2442
+ this._eventBus = eventBus;
2443
+ this._formFields = {};
2444
+ eventBus.on('form.clear', () => this.clear());
2445
+ this._ids = new Ids([32, 36, 1]);
2119
2446
  }
2120
-
2121
- /**
2122
- * @param {any} formField
2123
- * @param {string} [parentId]
2124
- *
2125
- * @return {any} importedField
2126
- */
2127
- importFormField(formField, parentId) {
2447
+ add(formField) {
2128
2448
  const {
2129
- components,
2130
- key,
2131
- type,
2132
- id = generateIdForType(type)
2133
- } = formField;
2134
- if (parentId) {
2135
- // set form field parent
2136
- formField._parent = parentId;
2137
- }
2138
- if (!this._formFields.get(type)) {
2139
- throw new Error(`form field of type <${type}> not supported`);
2140
- }
2141
- if (key) {
2142
- // validate <key> uniqueness
2143
- if (this._formFieldRegistry._keys.assigned(key)) {
2144
- throw new Error(`form field with key <${key}> already exists`);
2145
- }
2146
- this._formFieldRegistry._keys.claim(key, formField);
2147
-
2148
- // TODO: buttons should not have key
2149
- if (type !== 'button') {
2150
- // set form field path
2151
- formField._path = [key];
2152
- }
2153
- }
2154
- if (id) {
2155
- // validate <id> uniqueness
2156
- if (this._formFieldRegistry._ids.assigned(id)) {
2157
- throw new Error(`form field with id <${id}> already exists`);
2158
- }
2159
- this._formFieldRegistry._ids.claim(id, formField);
2160
- }
2161
-
2162
- // set form field ID
2163
- formField.id = id;
2164
- this._formFieldRegistry.add(formField);
2165
- if (components) {
2166
- this.importFormFields(components, id);
2449
+ id
2450
+ } = formField;
2451
+ if (this._formFields[id]) {
2452
+ throw new Error(`form field with ID ${id} already exists`);
2167
2453
  }
2168
- return formField;
2454
+ this._eventBus.fire('formField.add', {
2455
+ formField
2456
+ });
2457
+ this._formFields[id] = formField;
2169
2458
  }
2170
- importFormFields(components, parentId) {
2171
- components.forEach(component => {
2172
- this.importFormField(component, parentId);
2459
+ remove(formField) {
2460
+ const {
2461
+ id
2462
+ } = formField;
2463
+ if (!this._formFields[id]) {
2464
+ return;
2465
+ }
2466
+ this._eventBus.fire('formField.remove', {
2467
+ formField
2173
2468
  });
2469
+ delete this._formFields[id];
2174
2470
  }
2175
-
2176
- /**
2177
- * @param {Object} data
2178
- *
2179
- * @return {Object} initializedData
2180
- */
2181
- initializeFieldValues(data) {
2182
- return this._formFieldRegistry.getAll().reduce((initializedData, formField) => {
2183
- const {
2184
- defaultValue,
2185
- _path,
2186
- type
2187
- } = formField;
2188
-
2189
- // try to get value from data
2190
- // if unavailable - try to get default value from form field
2191
- // if unavailable - get empty value from form field
2192
-
2193
- if (_path) {
2194
- const {
2195
- config: fieldConfig
2196
- } = this._formFields.get(type);
2197
- let valueData = get(data, _path);
2198
- if (!isUndefined(valueData) && fieldConfig.sanitizeValue) {
2199
- valueData = fieldConfig.sanitizeValue({
2200
- formField,
2201
- data,
2202
- value: valueData
2203
- });
2204
- }
2205
- const initializedFieldValue = !isUndefined(valueData) ? valueData : !isUndefined(defaultValue) ? defaultValue : fieldConfig.emptyValue;
2206
- initializedData = {
2207
- ...initializedData,
2208
- [_path[0]]: initializedFieldValue
2209
- };
2210
- }
2211
- return initializedData;
2212
- }, data);
2471
+ get(id) {
2472
+ return this._formFields[id];
2473
+ }
2474
+ getAll() {
2475
+ return Object.values(this._formFields);
2476
+ }
2477
+ forEach(callback) {
2478
+ this.getAll().forEach(formField => callback(formField));
2479
+ }
2480
+ clear() {
2481
+ this._formFields = {};
2482
+ this._ids.clear();
2213
2483
  }
2214
2484
  }
2215
- Importer.$inject = ['formFieldRegistry', 'formFields', 'formLayouter'];
2216
-
2217
- var importModule = {
2218
- importer: ['type', Importer]
2219
- };
2485
+ FormFieldRegistry.$inject = ['eventBus'];
2220
2486
 
2221
2487
  function formFieldClasses(type, {
2222
2488
  errors = [],
@@ -2271,7 +2537,7 @@ function Button(props) {
2271
2537
  }
2272
2538
  Button.config = {
2273
2539
  type: type$c,
2274
- keyed: true,
2540
+ keyed: false,
2275
2541
  label: 'Button',
2276
2542
  group: 'action',
2277
2543
  create: (options = {}) => ({
@@ -2281,6 +2547,9 @@ Button.config = {
2281
2547
  };
2282
2548
 
2283
2549
  const FormRenderContext = createContext({
2550
+ EmptyRoot: props => {
2551
+ return null;
2552
+ },
2284
2553
  Empty: props => {
2285
2554
  return null;
2286
2555
  },
@@ -2310,6 +2579,10 @@ const FormRenderContext = createContext({
2310
2579
  class: props.class,
2311
2580
  children: props.children
2312
2581
  });
2582
+ },
2583
+ hoveredId: [],
2584
+ setHoveredId: newValue => {
2585
+ console.log(`setHoveredId not defined, called with '${newValue}'`);
2313
2586
  }
2314
2587
  });
2315
2588
  var FormRenderContext$1 = FormRenderContext;
@@ -2431,6 +2704,37 @@ function useReadonly(formField, properties = {}) {
2431
2704
  return readonly || false;
2432
2705
  }
2433
2706
 
2707
+ function usePrevious(value, defaultValue, dependencies) {
2708
+ const ref = useRef(defaultValue);
2709
+ useEffect(() => ref.current = value, dependencies);
2710
+ return ref.current;
2711
+ }
2712
+
2713
+ /**
2714
+ * A custom hook to manage state changes with deep comparison.
2715
+ *
2716
+ * @param {any} value - The current value to manage.
2717
+ * @param {any} defaultValue - The initial default value for the state.
2718
+ * @returns {any} - Returns the current state.
2719
+ */
2720
+ function useDeepCompareState(value, defaultValue) {
2721
+ const [state, setState] = useState(defaultValue);
2722
+ const previous = usePrevious(value, defaultValue, [value]);
2723
+ const changed = !compare(previous, value);
2724
+ useEffect(() => {
2725
+ if (changed) {
2726
+ setState(value);
2727
+ }
2728
+ }, [changed, value]);
2729
+ return state;
2730
+ }
2731
+
2732
+ // helpers //////////////////////////
2733
+
2734
+ function compare(a, b) {
2735
+ return JSON.stringify(a) === JSON.stringify(b);
2736
+ }
2737
+
2434
2738
  /**
2435
2739
  * Template a string reactively based on form data. If the string is not a template, it is returned as is.
2436
2740
  * Memoised to minimize re-renders
@@ -2651,6 +2955,21 @@ function _isReadableType(value) {
2651
2955
  function _isValueSomething(value) {
2652
2956
  return value || value === 0 || value === false;
2653
2957
  }
2958
+ function createEmptyOptions(options = {}) {
2959
+ const defaults = {};
2960
+
2961
+ // provide default values if valuesKey and valuesExpression are not set
2962
+ if (!options.valuesKey && !options.valuesExpression) {
2963
+ defaults.values = [{
2964
+ label: 'Value',
2965
+ value: 'value'
2966
+ }];
2967
+ }
2968
+ return {
2969
+ ...defaults,
2970
+ ...options
2971
+ };
2972
+ }
2654
2973
 
2655
2974
  /**
2656
2975
  * @enum { String }
@@ -2685,11 +3004,8 @@ function useValuesAsync (field) {
2685
3004
  state: LOAD_STATES.LOADING
2686
3005
  });
2687
3006
  const initialData = useService('form')._getState().initialData;
2688
- const evaluatedValues = useMemo(() => {
2689
- if (valuesExpression) {
2690
- return useExpressionEvaluation(valuesExpression);
2691
- }
2692
- }, [valuesExpression]);
3007
+ const expressionEvaluation = useExpressionEvaluation(valuesExpression);
3008
+ const evaluatedValues = useDeepCompareState(expressionEvaluation || [], []);
2693
3009
  useEffect(() => {
2694
3010
  let values = [];
2695
3011
 
@@ -2705,8 +3021,10 @@ function useValuesAsync (field) {
2705
3021
  values = Array.isArray(staticValues) ? staticValues : [];
2706
3022
 
2707
3023
  // expression
2708
- } else if (evaluatedValues && Array.isArray(evaluatedValues)) {
2709
- values = evaluatedValues;
3024
+ } else if (valuesExpression) {
3025
+ if (evaluatedValues && Array.isArray(evaluatedValues)) {
3026
+ values = evaluatedValues;
3027
+ }
2710
3028
  } else {
2711
3029
  setValuesGetter(buildErrorState('No values source defined in the form definition'));
2712
3030
  return;
@@ -2715,7 +3033,7 @@ function useValuesAsync (field) {
2715
3033
  // normalize data to support primitives and partially defined objects
2716
3034
  values = normalizeValuesData(values);
2717
3035
  setValuesGetter(buildLoadedState(values));
2718
- }, [valuesKey, staticValues, initialData]);
3036
+ }, [valuesKey, staticValues, initialData, valuesExpression, evaluatedValues]);
2719
3037
  return valuesGetter;
2720
3038
  }
2721
3039
  const buildErrorState = error => ({
@@ -3027,21 +3345,7 @@ Checklist.config = {
3027
3345
  group: 'selection',
3028
3346
  emptyValue: [],
3029
3347
  sanitizeValue: sanitizeMultiSelectValue,
3030
- create: (options = {}) => {
3031
- const defaults = {};
3032
-
3033
- // provide default values if valuesKey isn't set
3034
- if (!options.valuesKey) {
3035
- defaults.values = [{
3036
- label: 'Value',
3037
- value: 'value'
3038
- }];
3039
- }
3040
- return {
3041
- ...defaults,
3042
- ...options
3043
- };
3044
- }
3348
+ create: createEmptyOptions
3045
3349
  };
3046
3350
 
3047
3351
  const noop$1 = () => false;
@@ -3052,6 +3356,7 @@ function FormField(props) {
3052
3356
  } = props;
3053
3357
  const formFields = useService('formFields'),
3054
3358
  viewerCommands = useService('viewerCommands', false),
3359
+ pathRegistry = useService('pathRegistry'),
3055
3360
  form = useService('form');
3056
3361
  const {
3057
3362
  initialData,
@@ -3068,10 +3373,10 @@ function FormField(props) {
3068
3373
  if (!FormFieldComponent) {
3069
3374
  throw new Error(`cannot render field <${field.type}>`);
3070
3375
  }
3071
- const initialValue = useMemo(() => get(initialData, field._path), [initialData, field._path]);
3072
- const value = get(data, field._path);
3073
- const fieldErrors = findErrors(errors, field._path);
3376
+ const valuePath = useMemo(() => pathRegistry.getValuePath(field), [field, pathRegistry]);
3377
+ const initialValue = useMemo(() => get(initialData, valuePath), [initialData, valuePath]);
3074
3378
  const readonly = useReadonly(field, properties);
3379
+ const value = get(data, valuePath);
3075
3380
 
3076
3381
  // add precedence: global readonly > form field disabled
3077
3382
  const disabled = !properties.readOnly && (properties.disabled || field.disabled || false);
@@ -3098,7 +3403,7 @@ function FormField(props) {
3098
3403
  children: jsx(FormFieldComponent, {
3099
3404
  ...props,
3100
3405
  disabled: disabled,
3101
- errors: fieldErrors,
3406
+ errors: errors[field.id],
3102
3407
  onChange: disabled || readonly ? noop$1 : onChange,
3103
3408
  onBlur: disabled || readonly ? noop$1 : onBlur,
3104
3409
  readonly: readonly,
@@ -3108,14 +3413,14 @@ function FormField(props) {
3108
3413
  });
3109
3414
  }
3110
3415
 
3111
- function Default(props) {
3416
+ function Grid(props) {
3112
3417
  const {
3113
3418
  Children,
3114
- Empty,
3115
3419
  Row
3116
3420
  } = useContext(FormRenderContext$1);
3117
3421
  const {
3118
- field
3422
+ field,
3423
+ Empty
3119
3424
  } = props;
3120
3425
  const {
3121
3426
  id,
@@ -3152,7 +3457,20 @@ function Default(props) {
3152
3457
  }), components.length ? null : jsx(Empty, {})]
3153
3458
  });
3154
3459
  }
3155
- Default.config = {
3460
+
3461
+ function FormComponent$1(props) {
3462
+ const {
3463
+ EmptyRoot
3464
+ } = useContext(FormRenderContext$1);
3465
+ const fullProps = {
3466
+ ...props,
3467
+ Empty: EmptyRoot
3468
+ };
3469
+ return jsx(Grid, {
3470
+ ...fullProps
3471
+ });
3472
+ }
3473
+ FormComponent$1.config = {
3156
3474
  type: 'default',
3157
3475
  keyed: false,
3158
3476
  label: null,
@@ -3181,6 +3499,74 @@ var SvgCalendar = function SvgCalendar(props) {
3181
3499
  };
3182
3500
  var CalendarIcon = SvgCalendar;
3183
3501
 
3502
+ /**
3503
+ * Returns date format for the provided locale.
3504
+ * If the locale is not provided, uses the browser's locale.
3505
+ *
3506
+ * @param {string} [locale] - The locale to get date format for.
3507
+ * @returns {string} The date format for the locale.
3508
+ */
3509
+ function getLocaleDateFormat(locale = 'default') {
3510
+ const parts = new Intl.DateTimeFormat(locale).formatToParts(new Date(Date.UTC(2020, 5, 5)));
3511
+ return parts.map(part => {
3512
+ const len = part.value.length;
3513
+ switch (part.type) {
3514
+ case 'day':
3515
+ return 'd'.repeat(len);
3516
+ case 'month':
3517
+ return 'M'.repeat(len);
3518
+ case 'year':
3519
+ return 'y'.repeat(len);
3520
+ default:
3521
+ return part.value;
3522
+ }
3523
+ }).join('');
3524
+ }
3525
+
3526
+ /**
3527
+ * Returns readable date format for the provided locale.
3528
+ * If the locale is not provided, uses the browser's locale.
3529
+ *
3530
+ * @param {string} [locale] - The locale to get readable date format for.
3531
+ * @returns {string} The readable date format for the locale.
3532
+ */
3533
+ function getLocaleReadableDateFormat(locale) {
3534
+ let format = getLocaleDateFormat(locale).toLowerCase();
3535
+
3536
+ // Ensure month is in 'mm' format
3537
+ if (!format.includes('mm')) {
3538
+ format = format.replace('m', 'mm');
3539
+ }
3540
+
3541
+ // Ensure day is in 'dd' format
3542
+ if (!format.includes('dd')) {
3543
+ format = format.replace('d', 'dd');
3544
+ }
3545
+ return format;
3546
+ }
3547
+
3548
+ /**
3549
+ * Returns flatpickr config for the provided locale.
3550
+ * If the locale is not provided, uses the browser's locale.
3551
+ *
3552
+ * @param {string} [locale] - The locale to get flatpickr config for.
3553
+ * @returns {object} The flatpickr config for the locale.
3554
+ */
3555
+ function getLocaleDateFlatpickrConfig(locale) {
3556
+ return flatpickerizeDateFormat(getLocaleDateFormat(locale));
3557
+ }
3558
+ function flatpickerizeDateFormat(dateFormat) {
3559
+ const useLeadingZero = {
3560
+ day: dateFormat.includes('dd'),
3561
+ month: dateFormat.includes('MM'),
3562
+ year: dateFormat.includes('yyyy')
3563
+ };
3564
+ dateFormat = useLeadingZero.day ? dateFormat.replace('dd', 'd') : dateFormat.replace('d', 'j');
3565
+ dateFormat = useLeadingZero.month ? dateFormat.replace('MM', 'm') : dateFormat.replace('M', 'n');
3566
+ dateFormat = useLeadingZero.year ? dateFormat.replace('yyyy', 'Y') : dateFormat.replace('yy', 'y');
3567
+ return dateFormat;
3568
+ }
3569
+
3184
3570
  function InputAdorner(props) {
3185
3571
  const {
3186
3572
  pre,
@@ -3255,7 +3641,7 @@ function Datepicker(props) {
3255
3641
  useEffect(() => {
3256
3642
  let config = {
3257
3643
  allowInput: true,
3258
- dateFormat: 'm/d/Y',
3644
+ dateFormat: getLocaleDateFlatpickrConfig(),
3259
3645
  static: true,
3260
3646
  clickOpens: false,
3261
3647
  // TODO: support dates prior to 1900 (https://github.com/bpmn-io/form-js/issues/533)
@@ -3347,7 +3733,7 @@ function Datepicker(props) {
3347
3733
  class: "fjs-input",
3348
3734
  disabled: disabled,
3349
3735
  readOnly: readonly,
3350
- placeholder: "mm/dd/yyyy",
3736
+ placeholder: getLocaleReadableDateFormat(),
3351
3737
  autoComplete: "off",
3352
3738
  onFocus: onInputFocus,
3353
3739
  onKeyDown: onInputKeyDown,
@@ -3432,7 +3818,8 @@ function DropdownList(props) {
3432
3818
  useEffect(() => {
3433
3819
  const individualEntries = dropdownContainer.current.children;
3434
3820
  if (individualEntries.length && !mouseControl) {
3435
- individualEntries[focusedValueIndex].scrollIntoView({
3821
+ const focusedEntry = individualEntries[focusedValueIndex];
3822
+ focusedEntry && focusedEntry.scrollIntoView({
3436
3823
  block: 'nearest',
3437
3824
  inline: 'nearest'
3438
3825
  });
@@ -3968,6 +4355,52 @@ function FormComponent(props) {
3968
4355
  });
3969
4356
  }
3970
4357
 
4358
+ function Group(props) {
4359
+ const {
4360
+ field
4361
+ } = props;
4362
+ const {
4363
+ label,
4364
+ id,
4365
+ type,
4366
+ showOutline
4367
+ } = field;
4368
+ const {
4369
+ formId
4370
+ } = useContext(FormContext$1);
4371
+ const {
4372
+ Empty
4373
+ } = useContext(FormRenderContext$1);
4374
+ const fullProps = {
4375
+ ...props,
4376
+ Empty
4377
+ };
4378
+ return jsxs("div", {
4379
+ className: classNames(formFieldClasses(type), {
4380
+ 'fjs-outlined': showOutline
4381
+ }),
4382
+ role: "group",
4383
+ "aria-labelledby": prefixId(id, formId),
4384
+ children: [jsx(Label, {
4385
+ id: prefixId(id, formId),
4386
+ label: label
4387
+ }), jsx(Grid, {
4388
+ ...fullProps
4389
+ })]
4390
+ });
4391
+ }
4392
+ Group.config = {
4393
+ type: 'group',
4394
+ pathed: true,
4395
+ label: 'Group',
4396
+ group: 'presentation',
4397
+ create: (options = {}) => ({
4398
+ components: [],
4399
+ showOutline: true,
4400
+ ...options
4401
+ })
4402
+ };
4403
+
3971
4404
  const NODE_TYPE_TEXT = 3,
3972
4405
  NODE_TYPE_ELEMENT = 1;
3973
4406
  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'];
@@ -4556,21 +4989,7 @@ Radio.config = {
4556
4989
  group: 'selection',
4557
4990
  emptyValue: null,
4558
4991
  sanitizeValue: sanitizeSingleSelectValue,
4559
- create: (options = {}) => {
4560
- const defaults = {};
4561
-
4562
- // provide default values if valuesKey isn't set
4563
- if (!options.valuesKey) {
4564
- defaults.values = [{
4565
- label: 'Value',
4566
- value: 'value'
4567
- }];
4568
- }
4569
- return {
4570
- ...defaults,
4571
- ...options
4572
- };
4573
- }
4992
+ create: createEmptyOptions
4574
4993
  };
4575
4994
 
4576
4995
  var _path$d;
@@ -4922,21 +5341,7 @@ Select.config = {
4922
5341
  group: 'selection',
4923
5342
  emptyValue: null,
4924
5343
  sanitizeValue: sanitizeSingleSelectValue,
4925
- create: (options = {}) => {
4926
- const defaults = {};
4927
-
4928
- // provide default values if valuesKey isn't set
4929
- if (!options.valuesKey) {
4930
- defaults.values = [{
4931
- label: 'Value',
4932
- value: 'value'
4933
- }];
4934
- }
4935
- return {
4936
- ...defaults,
4937
- ...options
4938
- };
4939
- }
5344
+ create: createEmptyOptions
4940
5345
  };
4941
5346
 
4942
5347
  const type$4 = 'spacer';
@@ -5164,21 +5569,7 @@ Taglist.config = {
5164
5569
  group: 'selection',
5165
5570
  emptyValue: [],
5166
5571
  sanitizeValue: sanitizeMultiSelectValue,
5167
- create: (options = {}) => {
5168
- const defaults = {};
5169
-
5170
- // provide default values if valuesKey isn't set
5171
- if (!options.valuesKey) {
5172
- defaults.values = [{
5173
- label: 'Value',
5174
- value: 'value'
5175
- }];
5176
- }
5177
- return {
5178
- ...defaults,
5179
- ...options
5180
- };
5181
- }
5572
+ create: createEmptyOptions
5182
5573
  };
5183
5574
 
5184
5575
  const type$2 = 'text';
@@ -5595,13 +5986,14 @@ var SvgGroup = function SvgGroup(props) {
5595
5986
  return /*#__PURE__*/React.createElement("svg", _extends$8({
5596
5987
  xmlns: "http://www.w3.org/2000/svg",
5597
5988
  width: 54,
5598
- height: 54
5989
+ height: 54,
5990
+ fill: "currentcolor"
5599
5991
  }, props), _path$8 || (_path$8 = /*#__PURE__*/React.createElement("path", {
5600
5992
  fillRule: "evenodd",
5601
5993
  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"
5602
5994
  })));
5603
5995
  };
5604
- var ColumnsIcon = SvgGroup;
5996
+ var GroupIcon = SvgGroup;
5605
5997
 
5606
5998
  var _path$7;
5607
5999
  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); }
@@ -5654,14 +6046,14 @@ var SvgSpacer = function SvgSpacer(props) {
5654
6046
  xmlns: "http://www.w3.org/2000/svg",
5655
6047
  width: 54,
5656
6048
  height: 54,
5657
- fill: "none"
6049
+ fill: "currentcolor"
5658
6050
  }, props), _path$4 || (_path$4 = /*#__PURE__*/React.createElement("path", {
5659
- stroke: "#000",
6051
+ stroke: "currentcolor",
5660
6052
  strokeLinecap: "square",
5661
6053
  strokeWidth: 2,
5662
6054
  d: "M9 23h36M9 31h36"
5663
6055
  })), _path2$1 || (_path2$1 = /*#__PURE__*/React.createElement("path", {
5664
- stroke: "#000",
6056
+ stroke: "currentcolor",
5665
6057
  strokeLinecap: "round",
5666
6058
  strokeLinejoin: "round",
5667
6059
  strokeWidth: 2,
@@ -5739,8 +6131,9 @@ const iconsByType = type => {
5739
6131
  button: ButtonIcon,
5740
6132
  checkbox: CheckboxIcon,
5741
6133
  checklist: ChecklistIcon,
5742
- columns: ColumnsIcon,
6134
+ columns: GroupIcon,
5743
6135
  datetime: DatetimeIcon,
6136
+ group: GroupIcon,
5744
6137
  image: ImageIcon,
5745
6138
  number: NumberIcon,
5746
6139
  radio: RadioIcon,
@@ -5754,7 +6147,7 @@ const iconsByType = type => {
5754
6147
  }[type];
5755
6148
  };
5756
6149
 
5757
- const formFields = [Button, Checkbox, Checklist, Default, Image, Numberfield, Datetime, Radio, Select, Spacer, Taglist, Text, Textfield, Textarea];
6150
+ const formFields = [Button, Checkbox, Checklist, FormComponent$1, Group, Image, Numberfield, Datetime, Radio, Select, Spacer, Taglist, Text, Textfield, Textarea];
5758
6151
 
5759
6152
  class FormFields {
5760
6153
  constructor() {
@@ -5830,62 +6223,65 @@ var renderModule = {
5830
6223
  };
5831
6224
 
5832
6225
  var core = {
5833
- __depends__: [importModule, renderModule],
6226
+ __depends__: [renderModule],
5834
6227
  eventBus: ['type', EventBus],
6228
+ importer: ['type', Importer],
6229
+ fieldFactory: ['type', FieldFactory],
5835
6230
  formFieldRegistry: ['type', FormFieldRegistry],
6231
+ pathRegistry: ['type', PathRegistry],
5836
6232
  formLayouter: ['type', FormLayouter],
5837
6233
  validator: ['type', Validator]
5838
6234
  };
5839
6235
 
5840
- /**
5841
- * @typedef { import('./types').Injector } Injector
5842
- * @typedef { import('./types').Data } Data
5843
- * @typedef { import('./types').Errors } Errors
5844
- * @typedef { import('./types').Schema } Schema
5845
- * @typedef { import('./types').FormProperties } FormProperties
5846
- * @typedef { import('./types').FormProperty } FormProperty
5847
- * @typedef { import('./types').FormEvent } FormEvent
5848
- * @typedef { import('./types').FormOptions } FormOptions
5849
- *
5850
- * @typedef { {
5851
- * data: Data,
5852
- * initialData: Data,
5853
- * errors: Errors,
5854
- * properties: FormProperties,
5855
- * schema: Schema
5856
- * } } State
5857
- *
5858
- * @typedef { (type:FormEvent, priority:number, handler:Function) => void } OnEventWithPriority
5859
- * @typedef { (type:FormEvent, handler:Function) => void } OnEventWithOutPriority
5860
- * @typedef { OnEventWithPriority & OnEventWithOutPriority } OnEventType
6236
+ /**
6237
+ * @typedef { import('./types').Injector } Injector
6238
+ * @typedef { import('./types').Data } Data
6239
+ * @typedef { import('./types').Errors } Errors
6240
+ * @typedef { import('./types').Schema } Schema
6241
+ * @typedef { import('./types').FormProperties } FormProperties
6242
+ * @typedef { import('./types').FormProperty } FormProperty
6243
+ * @typedef { import('./types').FormEvent } FormEvent
6244
+ * @typedef { import('./types').FormOptions } FormOptions
6245
+ *
6246
+ * @typedef { {
6247
+ * data: Data,
6248
+ * initialData: Data,
6249
+ * errors: Errors,
6250
+ * properties: FormProperties,
6251
+ * schema: Schema
6252
+ * } } State
6253
+ *
6254
+ * @typedef { (type:FormEvent, priority:number, handler:Function) => void } OnEventWithPriority
6255
+ * @typedef { (type:FormEvent, handler:Function) => void } OnEventWithOutPriority
6256
+ * @typedef { OnEventWithPriority & OnEventWithOutPriority } OnEventType
5861
6257
  */
5862
6258
 
5863
6259
  const ids = new Ids([32, 36, 1]);
5864
6260
 
5865
- /**
5866
- * The form.
6261
+ /**
6262
+ * The form.
5867
6263
  */
5868
6264
  class Form {
5869
- /**
5870
- * @constructor
5871
- * @param {FormOptions} options
6265
+ /**
6266
+ * @constructor
6267
+ * @param {FormOptions} options
5872
6268
  */
5873
6269
  constructor(options = {}) {
5874
- /**
5875
- * @public
5876
- * @type {OnEventType}
6270
+ /**
6271
+ * @public
6272
+ * @type {OnEventType}
5877
6273
  */
5878
6274
  this.on = this._onEvent;
5879
6275
 
5880
- /**
5881
- * @public
5882
- * @type {String}
6276
+ /**
6277
+ * @public
6278
+ * @type {String}
5883
6279
  */
5884
6280
  this._id = ids.next();
5885
6281
 
5886
- /**
5887
- * @private
5888
- * @type {Element}
6282
+ /**
6283
+ * @private
6284
+ * @type {Element}
5889
6285
  */
5890
6286
  this._container = createFormContainer();
5891
6287
  const {
@@ -5894,9 +6290,9 @@ class Form {
5894
6290
  properties = {}
5895
6291
  } = options;
5896
6292
 
5897
- /**
5898
- * @private
5899
- * @type {State}
6293
+ /**
6294
+ * @private
6295
+ * @type {State}
5900
6296
  */
5901
6297
  this._state = {
5902
6298
  initialData: null,
@@ -5920,9 +6316,9 @@ class Form {
5920
6316
  this._emit('form.clear');
5921
6317
  }
5922
6318
 
5923
- /**
5924
- * Destroy the form, removing it from DOM,
5925
- * if attached.
6319
+ /**
6320
+ * Destroy the form, removing it from DOM,
6321
+ * if attached.
5926
6322
  */
5927
6323
  destroy() {
5928
6324
  // destroy form services
@@ -5933,13 +6329,13 @@ class Form {
5933
6329
  this._detach(false);
5934
6330
  }
5935
6331
 
5936
- /**
5937
- * Open a form schema with the given initial data.
5938
- *
5939
- * @param {Schema} schema
5940
- * @param {Data} [data]
5941
- *
5942
- * @return Promise<{ warnings: Array<any> }>
6332
+ /**
6333
+ * Open a form schema with the given initial data.
6334
+ *
6335
+ * @param {Schema} schema
6336
+ * @param {Data} [data]
6337
+ *
6338
+ * @return Promise<{ warnings: Array<any> }>
5943
6339
  */
5944
6340
  importSchema(schema, data = {}) {
5945
6341
  return new Promise((resolve, reject) => {
@@ -5947,9 +6343,9 @@ class Form {
5947
6343
  this.clear();
5948
6344
  const {
5949
6345
  schema: importedSchema,
5950
- data: initializedData,
5951
6346
  warnings
5952
- } = this.get('importer').importSchema(schema, data);
6347
+ } = this.get('importer').importSchema(schema);
6348
+ const initializedData = this._initializeFieldData(clone(data));
5953
6349
  this._setState({
5954
6350
  data: initializedData,
5955
6351
  errors: {},
@@ -5972,10 +6368,10 @@ class Form {
5972
6368
  });
5973
6369
  }
5974
6370
 
5975
- /**
5976
- * Submit the form, triggering all field validations.
5977
- *
5978
- * @returns { { data: Data, errors: Errors } }
6371
+ /**
6372
+ * Submit the form, triggering all field validations.
6373
+ *
6374
+ * @returns { { data: Data, errors: Errors } }
5979
6375
  */
5980
6376
  submit() {
5981
6377
  const {
@@ -6002,26 +6398,26 @@ class Form {
6002
6398
  });
6003
6399
  }
6004
6400
 
6005
- /**
6006
- * @returns {Errors}
6401
+ /**
6402
+ * @returns {Errors}
6007
6403
  */
6008
6404
  validate() {
6009
6405
  const formFieldRegistry = this.get('formFieldRegistry'),
6406
+ pathRegistry = this.get('pathRegistry'),
6010
6407
  validator = this.get('validator');
6011
6408
  const {
6012
6409
  data
6013
6410
  } = this._getState();
6014
6411
  const errors = formFieldRegistry.getAll().reduce((errors, field) => {
6015
6412
  const {
6016
- disabled,
6017
- _path
6413
+ disabled
6018
6414
  } = field;
6019
6415
  if (disabled) {
6020
6416
  return errors;
6021
6417
  }
6022
- const value = get(data, _path);
6418
+ const value = get(data, pathRegistry.getValuePath(field));
6023
6419
  const fieldErrors = validator.validateField(field, value);
6024
- return set(errors, [pathStringify(_path)], fieldErrors.length ? fieldErrors : undefined);
6420
+ return set(errors, [field.id], fieldErrors.length ? fieldErrors : undefined);
6025
6421
  }, /** @type {Errors} */{});
6026
6422
  this._setState({
6027
6423
  errors
@@ -6029,8 +6425,8 @@ class Form {
6029
6425
  return errors;
6030
6426
  }
6031
6427
 
6032
- /**
6033
- * @param {Element|string} parentNode
6428
+ /**
6429
+ * @param {Element|string} parentNode
6034
6430
  */
6035
6431
  attachTo(parentNode) {
6036
6432
  if (!parentNode) {
@@ -6048,10 +6444,10 @@ class Form {
6048
6444
  this._detach();
6049
6445
  }
6050
6446
 
6051
- /**
6052
- * @private
6053
- *
6054
- * @param {boolean} [emit]
6447
+ /**
6448
+ * @private
6449
+ *
6450
+ * @param {boolean} [emit]
6055
6451
  */
6056
6452
  _detach(emit = true) {
6057
6453
  const container = this._container,
@@ -6065,9 +6461,9 @@ class Form {
6065
6461
  parentNode.removeChild(container);
6066
6462
  }
6067
6463
 
6068
- /**
6069
- * @param {FormProperty} property
6070
- * @param {any} value
6464
+ /**
6465
+ * @param {FormProperty} property
6466
+ * @param {any} value
6071
6467
  */
6072
6468
  setProperty(property, value) {
6073
6469
  const properties = set(this._getState().properties, [property], value);
@@ -6076,21 +6472,21 @@ class Form {
6076
6472
  });
6077
6473
  }
6078
6474
 
6079
- /**
6080
- * @param {FormEvent} type
6081
- * @param {Function} handler
6475
+ /**
6476
+ * @param {FormEvent} type
6477
+ * @param {Function} handler
6082
6478
  */
6083
6479
  off(type, handler) {
6084
6480
  this.get('eventBus').off(type, handler);
6085
6481
  }
6086
6482
 
6087
- /**
6088
- * @private
6089
- *
6090
- * @param {FormOptions} options
6091
- * @param {Element} container
6092
- *
6093
- * @returns {Injector}
6483
+ /**
6484
+ * @private
6485
+ *
6486
+ * @param {FormOptions} options
6487
+ * @param {Element} container
6488
+ *
6489
+ * @returns {Injector}
6094
6490
  */
6095
6491
  _createInjector(options, container) {
6096
6492
  const {
@@ -6109,17 +6505,17 @@ class Form {
6109
6505
  }, core, ...modules, ...additionalModules]);
6110
6506
  }
6111
6507
 
6112
- /**
6113
- * @private
6508
+ /**
6509
+ * @private
6114
6510
  */
6115
6511
  _emit(type, data) {
6116
6512
  this.get('eventBus').fire(type, data);
6117
6513
  }
6118
6514
 
6119
- /**
6120
- * @internal
6121
- *
6122
- * @param { { add?: boolean, field: any, remove?: number, value?: any } } update
6515
+ /**
6516
+ * @internal
6517
+ *
6518
+ * @param { { add?: boolean, field: any, remove?: number, value?: any } } update
6123
6519
  */
6124
6520
  _update(update) {
6125
6521
  const {
@@ -6127,31 +6523,29 @@ class Form {
6127
6523
  value
6128
6524
  } = update;
6129
6525
  const {
6130
- _path
6131
- } = field;
6132
- let {
6133
6526
  data,
6134
6527
  errors
6135
6528
  } = this._getState();
6136
- const validator = this.get('validator');
6529
+ const validator = this.get('validator'),
6530
+ pathRegistry = this.get('pathRegistry');
6137
6531
  const fieldErrors = validator.validateField(field, value);
6138
- set(data, _path, value);
6139
- set(errors, [pathStringify(_path)], fieldErrors.length ? fieldErrors : undefined);
6532
+ set(data, pathRegistry.getValuePath(field), value);
6533
+ set(errors, [field.id], fieldErrors.length ? fieldErrors : undefined);
6140
6534
  this._setState({
6141
6535
  data: clone(data),
6142
6536
  errors: clone(errors)
6143
6537
  });
6144
6538
  }
6145
6539
 
6146
- /**
6147
- * @internal
6540
+ /**
6541
+ * @internal
6148
6542
  */
6149
6543
  _getState() {
6150
6544
  return this._state;
6151
6545
  }
6152
6546
 
6153
- /**
6154
- * @internal
6547
+ /**
6548
+ * @internal
6155
6549
  */
6156
6550
  _setState(state) {
6157
6551
  this._state = {
@@ -6161,56 +6555,96 @@ class Form {
6161
6555
  this._emit('changed', this._getState());
6162
6556
  }
6163
6557
 
6164
- /**
6165
- * @internal
6558
+ /**
6559
+ * @internal
6166
6560
  */
6167
6561
  _getModules() {
6168
6562
  return [ExpressionLanguageModule, MarkdownModule, ViewerCommandsModule];
6169
6563
  }
6170
6564
 
6171
- /**
6172
- * @internal
6565
+ /**
6566
+ * @internal
6173
6567
  */
6174
6568
  _onEvent(type, priority, handler) {
6175
6569
  this.get('eventBus').on(type, priority, handler);
6176
6570
  }
6177
6571
 
6178
- /**
6179
- * @internal
6572
+ /**
6573
+ * @internal
6180
6574
  */
6181
6575
  _getSubmitData() {
6182
- const formFieldRegistry = this.get('formFieldRegistry');
6576
+ const formFieldRegistry = this.get('formFieldRegistry'),
6577
+ pathRegistry = this.get('pathRegistry'),
6578
+ formFields = this.get('formFields');
6183
6579
  const formData = this._getState().data;
6184
6580
  const submitData = formFieldRegistry.getAll().reduce((previous, field) => {
6185
6581
  const {
6186
6582
  disabled,
6187
- _path
6583
+ type
6188
6584
  } = field;
6585
+ const {
6586
+ config: fieldConfig
6587
+ } = formFields.get(type);
6189
6588
 
6190
- // do not submit disabled form fields
6191
- if (disabled || !_path) {
6589
+ // do not submit disabled form fields or routing fields
6590
+ if (disabled || !fieldConfig.keyed) {
6192
6591
  return previous;
6193
6592
  }
6194
- const value = get(formData, _path);
6195
- return {
6196
- ...previous,
6197
- [_path[0]]: value
6198
- };
6593
+ const valuePath = pathRegistry.getValuePath(field);
6594
+ const value = get(formData, valuePath);
6595
+ return set(previous, valuePath, value);
6199
6596
  }, {});
6200
6597
  const filteredSubmitData = this._applyConditions(submitData, formData);
6201
6598
  return filteredSubmitData;
6202
6599
  }
6203
6600
 
6204
- /**
6205
- * @internal
6601
+ /**
6602
+ * @internal
6206
6603
  */
6207
6604
  _applyConditions(toFilter, data) {
6208
6605
  const conditionChecker = this.get('conditionChecker');
6209
6606
  return conditionChecker.applyConditions(toFilter, data);
6210
6607
  }
6608
+
6609
+ /**
6610
+ * @internal
6611
+ */
6612
+ _initializeFieldData(data) {
6613
+ const formFieldRegistry = this.get('formFieldRegistry'),
6614
+ formFields = this.get('formFields'),
6615
+ pathRegistry = this.get('pathRegistry');
6616
+ return formFieldRegistry.getAll().reduce((initializedData, formField) => {
6617
+ const {
6618
+ defaultValue,
6619
+ type
6620
+ } = formField;
6621
+
6622
+ // try to get value from data
6623
+ // if unavailable - try to get default value from form field
6624
+ // if unavailable - get empty value from form field
6625
+
6626
+ const valuePath = pathRegistry.getValuePath(formField);
6627
+ if (valuePath) {
6628
+ const {
6629
+ config: fieldConfig
6630
+ } = formFields.get(type);
6631
+ let valueData = get(data, valuePath);
6632
+ if (!isUndefined(valueData) && fieldConfig.sanitizeValue) {
6633
+ valueData = fieldConfig.sanitizeValue({
6634
+ formField,
6635
+ data,
6636
+ value: valueData
6637
+ });
6638
+ }
6639
+ const initializedFieldValue = !isUndefined(valueData) ? valueData : !isUndefined(defaultValue) ? defaultValue : fieldConfig.emptyValue;
6640
+ return set(initializedData, valuePath, initializedFieldValue);
6641
+ }
6642
+ return initializedData;
6643
+ }, data);
6644
+ }
6211
6645
  }
6212
6646
 
6213
- const schemaVersion = 10;
6647
+ const schemaVersion = 11;
6214
6648
 
6215
6649
  /**
6216
6650
  * @typedef { import('./types').CreateFormOptions } CreateFormOptions
@@ -6235,5 +6669,5 @@ function createForm(options) {
6235
6669
  });
6236
6670
  }
6237
6671
 
6238
- export { Button, Checkbox, Checklist, ConditionChecker, DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH, DATE_DISALLOW_PAST_PATH, DATE_LABEL_PATH, Datetime, Default, ExpressionLanguageModule, FeelExpressionLanguage, FeelersTemplating, Form, FormComponent, FormContext$1 as FormContext, FormFieldRegistry, FormFields, FormLayouter, FormRenderContext$1 as FormRenderContext, Image, MINUTES_IN_DAY, MarkdownModule, MarkdownRenderer, Numberfield, Radio, Select, Spacer, TIME_INTERVAL_PATH, TIME_LABEL_PATH, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISING_FORMATS, TIME_SERIALISING_FORMAT_PATH, TIME_USE24H_PATH, Taglist, Text, Textarea, Textfield, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS, VALUES_SOURCE_DEFAULT, ViewerCommands, ViewerCommandsModule, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getSchemaVariables, getValuesSource, iconsByType, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
6672
+ export { Button, Checkbox, Checklist, ConditionChecker, DATETIME_SUBTYPES, DATETIME_SUBTYPES_LABELS, DATETIME_SUBTYPE_PATH, DATE_DISALLOW_PAST_PATH, DATE_LABEL_PATH, Datetime, FormComponent$1 as Default, ExpressionLanguageModule, FeelExpressionLanguage, FeelersTemplating, FieldFactory, Form, FormComponent, FormContext$1 as FormContext, FormField, FormFieldRegistry, FormFields, FormLayouter, FormRenderContext$1 as FormRenderContext, Group, Image, Importer, MINUTES_IN_DAY, MarkdownModule, MarkdownRenderer, Numberfield, PathRegistry, Radio, Select, Spacer, TIME_INTERVAL_PATH, TIME_LABEL_PATH, TIME_SERIALISINGFORMAT_LABELS, TIME_SERIALISING_FORMATS, TIME_SERIALISING_FORMAT_PATH, TIME_USE24H_PATH, Taglist, Text, Textarea, Textfield, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_LABELS, VALUES_SOURCES_PATHS, VALUES_SOURCE_DEFAULT, ViewerCommands, ViewerCommandsModule, clone, createForm, createFormContainer, createInjector, formFields, generateIdForType, generateIndexForType, getSchemaVariables, getValuesSource, iconsByType, isRequired, pathParse, pathsEqual, runRecursively, schemaVersion };
6239
6673
  //# sourceMappingURL=index.es.js.map