@bpmn-io/form-js-viewer 0.10.0-alpha.0 → 0.10.0-alpha.2

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.
package/dist/index.cjs CHANGED
@@ -4,6 +4,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var Ids = require('ids');
6
6
  var minDash = require('min-dash');
7
+ var feelin = require('feelin');
8
+ var Big = require('big.js');
7
9
  var snarkdown = require('@bpmn-io/snarkdown');
8
10
  var classNames = require('classnames');
9
11
  var jsxRuntime = require('preact/jsx-runtime');
@@ -16,11 +18,132 @@ var didi = require('didi');
16
18
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
17
19
 
18
20
  var Ids__default = /*#__PURE__*/_interopDefaultLegacy(Ids);
21
+ var Big__default = /*#__PURE__*/_interopDefaultLegacy(Big);
19
22
  var snarkdown__default = /*#__PURE__*/_interopDefaultLegacy(snarkdown);
20
23
  var classNames__default = /*#__PURE__*/_interopDefaultLegacy(classNames);
21
24
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
22
25
  var Markup__default = /*#__PURE__*/_interopDefaultLegacy(Markup);
23
26
 
27
+ /**
28
+ * @typedef {object} Condition
29
+ * @property {string} [hide]
30
+ */
31
+
32
+ class ConditionChecker {
33
+ constructor(formFieldRegistry, eventBus) {
34
+ this._formFieldRegistry = formFieldRegistry;
35
+ this._eventBus = eventBus;
36
+ }
37
+
38
+ /**
39
+ * For given data, remove properties based on condition.
40
+ *
41
+ * @param {Object<string, any>} properties
42
+ * @param {Object<string, any>} data
43
+ */
44
+ applyConditions(properties, data = {}) {
45
+ const conditions = this._getConditions();
46
+ const newProperties = {
47
+ ...properties
48
+ };
49
+ for (const {
50
+ key,
51
+ condition
52
+ } of conditions) {
53
+ const shouldRemove = this._checkHideCondition(condition, data);
54
+ if (shouldRemove) {
55
+ delete newProperties[key];
56
+ }
57
+ }
58
+ return newProperties;
59
+ }
60
+
61
+ /**
62
+ * Check if given condition is met. Returns null for invalid/missing conditions.
63
+ *
64
+ * @param {string} condition
65
+ * @param {import('../types').Data} [data]
66
+ *
67
+ * @returns {boolean|null}
68
+ */
69
+ check(condition, data = {}) {
70
+ if (!condition) {
71
+ return null;
72
+ }
73
+ if (!minDash.isString(condition) || !condition.startsWith('=')) {
74
+ return null;
75
+ }
76
+ try {
77
+ // cut off initial '='
78
+ const result = feelin.unaryTest(condition.slice(1), data);
79
+ return result;
80
+ } catch (error) {
81
+ this._eventBus.fire('error', {
82
+ error
83
+ });
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Check if hide condition is met.
90
+ *
91
+ * @param {Condition} condition
92
+ * @param {Object<string, any>} data
93
+ * @returns {boolean}
94
+ */
95
+ _checkHideCondition(condition, data) {
96
+ if (!condition.hide) {
97
+ return false;
98
+ }
99
+ const result = this.check(condition.hide, data);
100
+ return result === true;
101
+ }
102
+
103
+ /**
104
+ * Evaluate an expression.
105
+ *
106
+ * @param {string} expression
107
+ * @param {import('../types').Data} [data]
108
+ *
109
+ * @returns {any}
110
+ */
111
+ evaluate(expression, data = {}) {
112
+ if (!expression) {
113
+ return null;
114
+ }
115
+ if (!minDash.isString(expression) || !expression.startsWith('=')) {
116
+ return null;
117
+ }
118
+ try {
119
+ const result = feelin.evaluate(expression.slice(1), data);
120
+ return result;
121
+ } catch (error) {
122
+ this._eventBus.fire('error', {
123
+ error
124
+ });
125
+ return null;
126
+ }
127
+ }
128
+ _getConditions() {
129
+ const formFields = this._formFieldRegistry.getAll();
130
+ return formFields.reduce((conditions, formField) => {
131
+ const {
132
+ key,
133
+ conditional: condition
134
+ } = formField;
135
+ if (key && condition) {
136
+ return [...conditions, {
137
+ key,
138
+ condition
139
+ }];
140
+ }
141
+ return conditions;
142
+ }, []);
143
+ }
144
+ }
145
+ ConditionChecker.$inject = ['formFieldRegistry', 'eventBus'];
146
+
24
147
  var FN_REF = '__fn';
25
148
  var DEFAULT_PRIORITY = 1000;
26
149
  var slice = Array.prototype.slice;
@@ -454,14 +577,63 @@ function invokeFunction(fn, args) {
454
577
  return fn.apply(null, args);
455
578
  }
456
579
 
580
+ function countDecimals(number) {
581
+ const num = Big__default['default'](number);
582
+ if (num.toString() === num.toFixed(0)) return 0;
583
+ return num.toFixed().split('.')[1].length || 0;
584
+ }
585
+ function isValidNumber(value) {
586
+ return (typeof value === 'number' || typeof value === 'string') && value !== '' && !isNaN(Number(value));
587
+ }
588
+ function willKeyProduceValidNumber(key, previousValue, carretIndex, selectionWidth, decimalDigits) {
589
+ // Dot and comma are both treated as dot
590
+ previousValue = previousValue.replace(',', '.');
591
+ const isFirstDot = !previousValue.includes('.') && (key === '.' || key === ',');
592
+ const isFirstMinus = !previousValue.includes('-') && key === '-' && carretIndex === 0;
593
+ const keypressIsNumeric = /^[0-9]$/i.test(key);
594
+ const dotIndex = previousValue?.indexOf('.') ?? -1;
595
+
596
+ // If the carret is positioned after a dot, and the current decimal digits count is equal or greater to the maximum, disallow the key press
597
+ const overflowsDecimalSpace = typeof decimalDigits === 'number' && selectionWidth === 0 && dotIndex !== -1 && previousValue.includes('.') && previousValue.split('.')[1].length >= decimalDigits && carretIndex > dotIndex;
598
+ const keypressIsAllowedChar = keypressIsNumeric || decimalDigits !== 0 && isFirstDot || isFirstMinus;
599
+ return keypressIsAllowedChar && !overflowsDecimalSpace;
600
+ }
601
+ function isNullEquivalentValue(value) {
602
+ return value === undefined || value === null || value === '';
603
+ }
604
+
457
605
  const EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
458
606
  const PHONE_PATTERN = /(\+|00)(297|93|244|1264|358|355|376|971|54|374|1684|1268|61|43|994|257|32|229|226|880|359|973|1242|387|590|375|501|1441|591|55|1246|673|975|267|236|1|61|41|56|86|225|237|243|242|682|57|269|238|506|53|5999|61|1345|357|420|49|253|1767|45|1809|1829|1849|213|593|20|291|212|34|372|251|358|679|500|33|298|691|241|44|995|44|233|350|224|590|220|245|240|30|1473|299|502|594|1671|592|852|504|385|509|36|62|44|91|246|353|98|964|354|972|39|1876|44|962|81|76|77|254|996|855|686|1869|82|383|965|856|961|231|218|1758|423|94|266|370|352|371|853|590|212|377|373|261|960|52|692|389|223|356|95|382|976|1670|258|222|1664|596|230|265|60|262|264|687|227|672|234|505|683|31|47|977|674|64|968|92|507|64|51|63|680|675|48|1787|1939|850|351|595|970|689|974|262|40|7|250|966|249|221|65|500|4779|677|232|503|378|252|508|381|211|239|597|421|386|46|268|1721|248|963|1649|235|228|66|992|690|993|670|676|1868|216|90|688|886|255|256|380|598|1|998|3906698|379|1784|58|1284|1340|84|678|681|685|967|27|260|263)(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{4,20}$/;
459
607
  class Validator {
460
608
  validateField(field, value) {
461
609
  const {
610
+ type,
462
611
  validate
463
612
  } = field;
464
613
  let errors = [];
614
+ if (type === 'number') {
615
+ const {
616
+ decimalDigits,
617
+ step
618
+ } = field;
619
+ if (value === 'NaN') {
620
+ errors = [...errors, 'Value is not a number.'];
621
+ } else if (value) {
622
+ if (decimalDigits >= 0 && countDecimals(value) > decimalDigits) {
623
+ errors = [...errors, 'Value is expected to ' + (decimalDigits === 0 ? 'be an integer' : `have at most ${decimalDigits} decimal digit${decimalDigits > 1 ? 's' : ''}`) + '.'];
624
+ }
625
+ if (step) {
626
+ const bigValue = Big__default['default'](value);
627
+ const bigStep = Big__default['default'](step);
628
+ const offset = bigValue.mod(bigStep);
629
+ if (offset.cmp(0) !== 0) {
630
+ const previousValue = bigValue.minus(offset);
631
+ const nextValue = previousValue.plus(bigStep);
632
+ errors = [...errors, `Please select a valid value, the two nearest valid values are ${previousValue} and ${nextValue}.`];
633
+ }
634
+ }
635
+ }
636
+ }
465
637
  if (!validate) {
466
638
  return errors;
467
639
  }
@@ -543,6 +715,44 @@ class FormFieldRegistry {
543
715
  }
544
716
  FormFieldRegistry.$inject = ['eventBus'];
545
717
 
718
+ /**
719
+ * Retrieve variable names from given FEEL unary test.
720
+ *
721
+ * @param {string} unaryTest
722
+ * @returns {string[]}
723
+ */
724
+ function getVariableNames(unaryTest) {
725
+ const tree = feelin.parseUnaryTests(unaryTest);
726
+ const cursor = tree.cursor();
727
+ const variables = new Set();
728
+ do {
729
+ const node = cursor.node;
730
+ if (node.type.name === 'VariableName') {
731
+ variables.add(unaryTest.slice(node.from, node.to));
732
+ }
733
+ } while (cursor.next());
734
+ return Array.from(variables);
735
+ }
736
+
737
+ /**
738
+ * Retrieve variable names from given FEEL expression.
739
+ *
740
+ * @param {string} expression
741
+ * @returns {string[]}
742
+ */
743
+ function getExpressionVariableNames(expression) {
744
+ const tree = feelin.parseExpressions(expression);
745
+ const cursor = tree.cursor();
746
+ const variables = new Set();
747
+ do {
748
+ const node = cursor.node;
749
+ if (node.type.name === 'VariableName') {
750
+ variables.add(expression.slice(node.from, node.to));
751
+ }
752
+ } while (cursor.next());
753
+ return Array.from(variables);
754
+ }
755
+
546
756
  function createInjector(bootstrapModules) {
547
757
  const injector = new didi.Injector(bootstrapModules);
548
758
  injector.init();
@@ -560,6 +770,7 @@ function createFormContainer(prefix = 'fjs') {
560
770
  return container;
561
771
  }
562
772
 
773
+ const EXPRESSION_PROPERTIES = ['alt', 'source'];
563
774
  function findErrors(errors, path) {
564
775
  return errors[pathStringify(path)];
565
776
  }
@@ -613,16 +824,16 @@ function clone(data, replacer) {
613
824
  *
614
825
  * @return {string[]}
615
826
  */
616
-
617
827
  function getSchemaVariables(schema) {
618
828
  if (!schema.components) {
619
829
  return [];
620
830
  }
621
- return schema.components.reduce((variables, component) => {
831
+ const variables = schema.components.reduce((variables, component) => {
622
832
  const {
623
833
  key,
624
834
  valuesKey,
625
- type
835
+ type,
836
+ conditional
626
837
  } = component;
627
838
  if (['text', 'button'].includes(type)) {
628
839
  return variables;
@@ -630,11 +841,33 @@ function getSchemaVariables(schema) {
630
841
  if (key) {
631
842
  variables = [...variables, key];
632
843
  }
633
- if (valuesKey && !variables.includes(valuesKey)) {
844
+ if (valuesKey) {
634
845
  variables = [...variables, valuesKey];
635
846
  }
847
+ if (conditional && conditional.hide) {
848
+ // cut off initial '='
849
+ const conditionVariables = getVariableNames(conditional.hide.slice(1));
850
+ variables = [...variables, ...conditionVariables];
851
+ }
852
+ EXPRESSION_PROPERTIES.forEach(prop => {
853
+ const property = component[prop];
854
+ if (property && isExpression$1(property)) {
855
+ // cut off initial '='
856
+ const expressionVariables = getExpressionVariableNames(property.slice(1));
857
+ variables = [...variables, ...expressionVariables];
858
+ }
859
+ });
636
860
  return variables;
637
861
  }, []);
862
+
863
+ // remove duplicates
864
+ return Array.from(new Set(variables));
865
+ }
866
+
867
+ // helper ///////////////
868
+
869
+ function isExpression$1(value) {
870
+ return minDash.isString(value) && value.startsWith('=');
638
871
  }
639
872
 
640
873
  class Importer {
@@ -663,11 +896,11 @@ class Importer {
663
896
  const warnings = [];
664
897
  try {
665
898
  const importedSchema = this.importFormField(clone(schema)),
666
- importedData = this.importData(clone(data));
899
+ initializedData = this.initializeFieldValues(clone(data));
667
900
  return {
668
901
  warnings,
669
902
  schema: importedSchema,
670
- data: importedData
903
+ data: initializedData
671
904
  };
672
905
  } catch (err) {
673
906
  err.warnings = warnings;
@@ -733,26 +966,16 @@ class Importer {
733
966
  /**
734
967
  * @param {Object} data
735
968
  *
736
- * @return {Object} importedData
969
+ * @return {Object} initializedData
737
970
  */
738
- importData(data) {
739
- return this._formFieldRegistry.getAll().reduce((importedData, formField) => {
971
+ initializeFieldValues(data) {
972
+ return this._formFieldRegistry.getAll().reduce((initializedData, formField) => {
740
973
  const {
741
974
  defaultValue,
742
975
  _path,
743
- type,
744
- valuesKey
976
+ type
745
977
  } = formField;
746
978
 
747
- // get values defined via valuesKey
748
-
749
- if (valuesKey) {
750
- importedData = {
751
- ...importedData,
752
- [valuesKey]: minDash.get(data, [valuesKey])
753
- };
754
- }
755
-
756
979
  // try to get value from data
757
980
  // if unavailable - try to get default value from form field
758
981
  // if unavailable - get empty value from form field
@@ -767,14 +990,14 @@ class Importer {
767
990
  value: valueData
768
991
  });
769
992
  }
770
- const initialFieldValue = !minDash.isUndefined(valueData) ? valueData : !minDash.isUndefined(defaultValue) ? defaultValue : fieldImplementation.emptyValue;
771
- importedData = {
772
- ...importedData,
773
- [_path[0]]: initialFieldValue
993
+ const initializedFieldValue = !minDash.isUndefined(valueData) ? valueData : !minDash.isUndefined(defaultValue) ? defaultValue : fieldImplementation.emptyValue;
994
+ initializedData = {
995
+ ...initializedData,
996
+ [_path[0]]: initializedFieldValue
774
997
  };
775
998
  }
776
- return importedData;
777
- }, {});
999
+ return initializedData;
1000
+ }, data);
778
1001
  }
779
1002
  }
780
1003
  Importer.$inject = ['formFieldRegistry', 'formFields'];
@@ -788,6 +1011,7 @@ const NODE_TYPE_TEXT = 3,
788
1011
  const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong'];
789
1012
  const ALLOWED_ATTRIBUTES = ['align', 'alt', 'class', 'href', 'id', 'name', 'rel', 'target', 'src'];
790
1013
  const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i; // eslint-disable-line no-useless-escape
1014
+ const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
791
1015
  const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
792
1016
 
793
1017
  const FORM_ELEMENT = document.createElement('form');
@@ -811,6 +1035,10 @@ function sanitizeHTML(html) {
811
1035
  return '';
812
1036
  }
813
1037
  }
1038
+ function sanitizeImageSource(src) {
1039
+ const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
1040
+ return valid ? src : '';
1041
+ }
814
1042
 
815
1043
  /**
816
1044
  * Recursively sanitize a HTML node, potentially
@@ -921,6 +1149,19 @@ function safeMarkdown(markdown) {
921
1149
  const html = markdownToHTML(markdown);
922
1150
  return sanitizeHTML(html);
923
1151
  }
1152
+
1153
+ /**
1154
+ * Sanitizes an image source to ensure we only allow for data URI and links
1155
+ * that start with http(s).
1156
+ *
1157
+ * Note: Most browsers anyway do not support script execution in <img> elements.
1158
+ *
1159
+ * @param {string} src
1160
+ * @returns {string}
1161
+ */
1162
+ function safeImageSource(src) {
1163
+ return sanitizeImageSource(src);
1164
+ }
924
1165
  function sanitizeSingleSelectValue(options) {
925
1166
  const {
926
1167
  formField,
@@ -960,7 +1201,7 @@ function sanitizeMultiSelectValue(options) {
960
1201
  }
961
1202
  }
962
1203
 
963
- const type$9 = 'button';
1204
+ const type$a = 'button';
964
1205
  function Button(props) {
965
1206
  const {
966
1207
  disabled,
@@ -970,7 +1211,7 @@ function Button(props) {
970
1211
  action = 'submit'
971
1212
  } = field;
972
1213
  return jsxRuntime.jsx("div", {
973
- class: formFieldClasses(type$9),
1214
+ class: formFieldClasses(type$a),
974
1215
  children: jsxRuntime.jsx("button", {
975
1216
  class: "fjs-button",
976
1217
  type: action,
@@ -985,7 +1226,7 @@ Button.create = function (options = {}) {
985
1226
  ...options
986
1227
  };
987
1228
  };
988
- Button.type = type$9;
1229
+ Button.type = type$a;
989
1230
  Button.label = 'Button';
990
1231
  Button.keyed = true;
991
1232
 
@@ -1061,7 +1302,7 @@ function Label(props) {
1061
1302
  });
1062
1303
  }
1063
1304
 
1064
- const type$8 = 'checkbox';
1305
+ const type$9 = 'checkbox';
1065
1306
  function Checkbox(props) {
1066
1307
  const {
1067
1308
  disabled,
@@ -1086,7 +1327,7 @@ function Checkbox(props) {
1086
1327
  formId
1087
1328
  } = hooks.useContext(FormContext);
1088
1329
  return jsxRuntime.jsxs("div", {
1089
- class: classNames__default['default'](formFieldClasses(type$8, {
1330
+ class: classNames__default['default'](formFieldClasses(type$9, {
1090
1331
  errors,
1091
1332
  disabled
1092
1333
  }), {
@@ -1116,7 +1357,7 @@ Checkbox.create = function (options = {}) {
1116
1357
  ...options
1117
1358
  };
1118
1359
  };
1119
- Checkbox.type = type$8;
1360
+ Checkbox.type = type$9;
1120
1361
  Checkbox.label = 'Checkbox';
1121
1362
  Checkbox.keyed = true;
1122
1363
  Checkbox.emptyValue = false;
@@ -1191,7 +1432,7 @@ const buildLoadedState = values => ({
1191
1432
  state: LOAD_STATES.LOADED
1192
1433
  });
1193
1434
 
1194
- const type$7 = 'checklist';
1435
+ const type$8 = 'checklist';
1195
1436
  function Checklist(props) {
1196
1437
  const {
1197
1438
  disabled,
@@ -1224,7 +1465,7 @@ function Checklist(props) {
1224
1465
  formId
1225
1466
  } = hooks.useContext(FormContext);
1226
1467
  return jsxRuntime.jsxs("div", {
1227
- class: classNames__default['default'](formFieldClasses(type$7, {
1468
+ class: classNames__default['default'](formFieldClasses(type$8, {
1228
1469
  errors,
1229
1470
  disabled
1230
1471
  })),
@@ -1264,12 +1505,28 @@ Checklist.create = function (options = {}) {
1264
1505
  ...options
1265
1506
  };
1266
1507
  };
1267
- Checklist.type = type$7;
1508
+ Checklist.type = type$8;
1268
1509
  Checklist.label = 'Checklist';
1269
1510
  Checklist.keyed = true;
1270
1511
  Checklist.emptyValue = [];
1271
1512
  Checklist.sanitizeValue = sanitizeMultiSelectValue;
1272
1513
 
1514
+ /**
1515
+ * Check if condition is met with given variables.
1516
+ *
1517
+ * @param {string | undefined} condition
1518
+ * @param {import('../../types').Data} data
1519
+ *
1520
+ * @returns {boolean} true if condition is met or no condition or condition checker exists
1521
+ */
1522
+ function useCondition(condition, data) {
1523
+ const conditionChecker = useService('conditionChecker', false);
1524
+ if (!conditionChecker) {
1525
+ return null;
1526
+ }
1527
+ return conditionChecker.check(condition, data);
1528
+ }
1529
+
1273
1530
  const noop$1 = () => false;
1274
1531
  function FormField(props) {
1275
1532
  const {
@@ -1287,7 +1544,8 @@ function FormField(props) {
1287
1544
  properties
1288
1545
  } = form._getState();
1289
1546
  const {
1290
- Element
1547
+ Element,
1548
+ Empty
1291
1549
  } = hooks.useContext(FormRenderContext);
1292
1550
  const FormFieldComponent = formFields.get(field.type);
1293
1551
  if (!FormFieldComponent) {
@@ -1296,6 +1554,10 @@ function FormField(props) {
1296
1554
  const value = minDash.get(data, _path);
1297
1555
  const fieldErrors = findErrors(errors, _path);
1298
1556
  const disabled = properties.readOnly || field.disabled || false;
1557
+ const hidden = useHideCondition(field, data);
1558
+ if (hidden) {
1559
+ return jsxRuntime.jsx(Empty, {});
1560
+ }
1299
1561
  return jsxRuntime.jsx(Element, {
1300
1562
  field: field,
1301
1563
  children: jsxRuntime.jsx(FormFieldComponent, {
@@ -1307,6 +1569,10 @@ function FormField(props) {
1307
1569
  })
1308
1570
  });
1309
1571
  }
1572
+ function useHideCondition(field, data) {
1573
+ const hideCondition = field.conditional && field.conditional.hide;
1574
+ return useCondition(hideCondition, data) === true;
1575
+ }
1310
1576
 
1311
1577
  function Default(props) {
1312
1578
  const {
@@ -1452,6 +1718,7 @@ function FormComponent(props) {
1452
1718
  class: "fjs-form",
1453
1719
  onSubmit: handleSubmit,
1454
1720
  onReset: handleReset,
1721
+ noValidate: true,
1455
1722
  children: [jsxRuntime.jsx(FormField, {
1456
1723
  field: schema,
1457
1724
  onChange: onChange
@@ -1459,32 +1726,226 @@ function FormComponent(props) {
1459
1726
  });
1460
1727
  }
1461
1728
 
1729
+ /**
1730
+ *
1731
+ * @param {string | undefined} expression
1732
+ * @param {import('../../types').Data} data
1733
+ */
1734
+ function useEvaluation(expression, data) {
1735
+ const conditionChecker = useService('conditionChecker', false);
1736
+ if (!conditionChecker) {
1737
+ return null;
1738
+ }
1739
+ return conditionChecker.evaluate(expression, data);
1740
+ }
1741
+
1742
+ /**
1743
+ *
1744
+ * @param {string} value
1745
+ */
1746
+ function useExpressionValue(value) {
1747
+ const formData = useService('form')._getState().data;
1748
+ if (!isExpression(value)) {
1749
+ return value;
1750
+ }
1751
+
1752
+ // We can ignore this hook rule as we do not use
1753
+ // state or effects in our custom hooks
1754
+ /* eslint-disable-next-line react-hooks/rules-of-hooks */
1755
+ return useEvaluation(value, formData);
1756
+ }
1757
+
1758
+ // helper ///////////////
1759
+
1760
+ function isExpression(value) {
1761
+ return minDash.isString(value) && value.startsWith('=');
1762
+ }
1763
+
1764
+ function _extends$1() { _extends$1 = 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$1.apply(this, arguments); }
1765
+ var ImagePlaceholder = (({
1766
+ styles = {},
1767
+ ...props
1768
+ }) => /*#__PURE__*/React__default['default'].createElement("svg", _extends$1({
1769
+ width: "64",
1770
+ height: "64",
1771
+ viewBox: "0 0 1280 1280",
1772
+ xmlns: "http://www.w3.org/2000/svg",
1773
+ fillRule: "evenodd",
1774
+ clipRule: "evenodd",
1775
+ strokeLinejoin: "round",
1776
+ strokeMiterlimit: "2"
1777
+ }, props), /*#__PURE__*/React__default['default'].createElement("path", {
1778
+ fill: "#e5e9ed",
1779
+ d: "M0 0h1280v1280H0z"
1780
+ }), /*#__PURE__*/React__default['default'].createElement("path", {
1781
+ d: "M910 410H370v470h540V410zm-57.333 57.333v355.334H427.333V467.333h425.334z",
1782
+ fill: "#cad3db"
1783
+ }), /*#__PURE__*/React__default['default'].createElement("path", {
1784
+ d: "M810 770H480v-60l100-170 130 170 100-65v125z",
1785
+ fill: "#cad3db"
1786
+ }), /*#__PURE__*/React__default['default'].createElement("circle", {
1787
+ cx: "750",
1788
+ cy: "550",
1789
+ r: "50",
1790
+ fill: "#cad3db",
1791
+ transform: "translate(10 10)"
1792
+ })));
1793
+
1794
+ const type$7 = 'image';
1795
+ function Image(props) {
1796
+ const {
1797
+ field
1798
+ } = props;
1799
+ const {
1800
+ alt,
1801
+ id,
1802
+ source
1803
+ } = field;
1804
+ const safeSource = safeImageSource(useExpressionValue(source));
1805
+ const altText = useExpressionValue(alt);
1806
+ const {
1807
+ formId
1808
+ } = hooks.useContext(FormContext);
1809
+ return jsxRuntime.jsx("div", {
1810
+ class: formFieldClasses(type$7),
1811
+ children: jsxRuntime.jsxs("div", {
1812
+ class: "fjs-image-container",
1813
+ children: [safeSource && jsxRuntime.jsx("img", {
1814
+ alt: altText,
1815
+ src: safeSource,
1816
+ class: "fjs-image",
1817
+ id: prefixId(id, formId)
1818
+ }), !safeSource && jsxRuntime.jsx("div", {
1819
+ class: "fjs-image-placeholder",
1820
+ children: jsxRuntime.jsx(ImagePlaceholder, {
1821
+ alt: "This is an image placeholder"
1822
+ })
1823
+ })]
1824
+ })
1825
+ });
1826
+ }
1827
+ Image.create = function (options = {}) {
1828
+ return {
1829
+ ...options
1830
+ };
1831
+ };
1832
+ Image.type = type$7;
1833
+ Image.keyed = false;
1834
+
1462
1835
  const type$6 = 'number';
1463
- function Number(props) {
1836
+ function Numberfield(props) {
1464
1837
  const {
1465
1838
  disabled,
1466
1839
  errors = [],
1467
1840
  field,
1468
- value
1841
+ value,
1842
+ onChange
1469
1843
  } = props;
1470
1844
  const {
1471
1845
  description,
1472
1846
  id,
1473
1847
  label,
1474
- validate = {}
1848
+ validate = {},
1849
+ decimalDigits,
1850
+ serializeToString = false,
1851
+ increment: incrementValue
1475
1852
  } = field;
1476
1853
  const {
1477
1854
  required
1478
1855
  } = validate;
1479
- const onChange = ({
1480
- target
1481
- }) => {
1482
- props.onChange({
1856
+ const inputRef = hooks.useRef();
1857
+ const [stringValueCache, setStringValueCache] = hooks.useState('');
1858
+
1859
+ // checks whether the value currently in the form data is practically different from the one in the input field cache
1860
+ // this allows us to guarantee the field always displays valid form data, but without auto-simplifying values like 1.000 to 1
1861
+ const cacheValueMatchesState = hooks.useMemo(() => Numberfield.sanitizeValue({
1862
+ value,
1863
+ formField: field
1864
+ }) === Numberfield.sanitizeValue({
1865
+ value: stringValueCache,
1866
+ formField: field
1867
+ }), [stringValueCache, value, field]);
1868
+ const displayValue = hooks.useMemo(() => {
1869
+ if (value === 'NaN') return 'NaN';
1870
+ return cacheValueMatchesState ? stringValueCache : value || value === 0 ? Big__default['default'](value).toFixed() : '';
1871
+ }, [stringValueCache, value, cacheValueMatchesState]);
1872
+ const arrowIncrementValue = hooks.useMemo(() => {
1873
+ if (incrementValue) return Big__default['default'](incrementValue);
1874
+ if (decimalDigits) return Big__default['default'](`1e-${decimalDigits}`);
1875
+ return Big__default['default']('1');
1876
+ }, [decimalDigits, incrementValue]);
1877
+ const setValue = hooks.useCallback(stringValue => {
1878
+ if (isNullEquivalentValue(stringValue)) {
1879
+ setStringValueCache('');
1880
+ onChange({
1881
+ field,
1882
+ value: null
1883
+ });
1884
+ return;
1885
+ }
1886
+
1887
+ // treat commas as dots
1888
+ stringValue = stringValue.replaceAll(',', '.');
1889
+ if (isNaN(Number(stringValue))) {
1890
+ setStringValueCache('NaN');
1891
+ onChange({
1892
+ field,
1893
+ value: 'NaN'
1894
+ });
1895
+ return;
1896
+ }
1897
+ setStringValueCache(stringValue);
1898
+ onChange({
1483
1899
  field,
1484
- value: Number.sanitizeValue({
1485
- value: target.value
1486
- })
1900
+ value: serializeToString ? stringValue : Number(stringValue)
1487
1901
  });
1902
+ }, [field, onChange, serializeToString]);
1903
+ const increment = () => {
1904
+ const base = isValidNumber(value) ? Big__default['default'](value) : Big__default['default'](0);
1905
+ const stepFlooredValue = base.minus(base.mod(arrowIncrementValue));
1906
+
1907
+ // note: toFixed() behaves differently in big.js
1908
+ setValue(stepFlooredValue.plus(arrowIncrementValue).toFixed());
1909
+ };
1910
+ const decrement = () => {
1911
+ const base = isValidNumber(value) ? Big__default['default'](value) : Big__default['default'](0);
1912
+ const offset = base.mod(arrowIncrementValue);
1913
+ if (offset.cmp(0) === 0) {
1914
+ // if we're already on a valid step, decrement
1915
+ setValue(base.minus(arrowIncrementValue).toFixed());
1916
+ } else {
1917
+ // otherwise floor to the step
1918
+ const stepFlooredValue = base.minus(base.mod(arrowIncrementValue));
1919
+ setValue(stepFlooredValue.toFixed());
1920
+ }
1921
+ };
1922
+ const onKeyDown = e => {
1923
+ // delete the NaN state all at once on backspace or delete
1924
+ if (value === 'NaN' && (e.code === 'Backspace' || e.code === 'Delete')) {
1925
+ setValue(null);
1926
+ e.preventDefault();
1927
+ return;
1928
+ }
1929
+ if (e.code === 'ArrowUp') {
1930
+ increment();
1931
+ e.preventDefault();
1932
+ return;
1933
+ }
1934
+ if (e.code === 'ArrowDown') {
1935
+ decrement();
1936
+ e.preventDefault();
1937
+ return;
1938
+ }
1939
+ };
1940
+
1941
+ // intercept key presses which would lead to an invalid number
1942
+ const onKeyPress = e => {
1943
+ const carretIndex = inputRef.current.selectionStart;
1944
+ const selectionWidth = inputRef.current.selectionStart - inputRef.current.selectionEnd;
1945
+ const previousValue = inputRef.current.value;
1946
+ if (!willKeyProduceValidNumber(e.key, previousValue, carretIndex, selectionWidth, decimalDigits)) {
1947
+ e.preventDefault();
1948
+ }
1488
1949
  };
1489
1950
  const {
1490
1951
  formId
@@ -1498,13 +1959,45 @@ function Number(props) {
1498
1959
  id: prefixId(id, formId),
1499
1960
  label: label,
1500
1961
  required: required
1501
- }), jsxRuntime.jsx("input", {
1502
- class: "fjs-input",
1503
- disabled: disabled,
1504
- id: prefixId(id, formId),
1505
- onInput: onChange,
1506
- type: "number",
1507
- value: value || ''
1962
+ }), jsxRuntime.jsxs("div", {
1963
+ class: classNames__default['default']('fjs-input-group', {
1964
+ 'disabled': disabled
1965
+ }, {
1966
+ 'hasErrors': errors.length
1967
+ }),
1968
+ children: [jsxRuntime.jsx("input", {
1969
+ ref: inputRef,
1970
+ class: "fjs-input",
1971
+ disabled: disabled,
1972
+ id: prefixId(id, formId),
1973
+ onKeyDown: onKeyDown,
1974
+ onKeyPress: onKeyPress
1975
+
1976
+ // @ts-ignore
1977
+ ,
1978
+ onInput: e => setValue(e.target.value),
1979
+ type: "text",
1980
+ autoComplete: "off",
1981
+ step: arrowIncrementValue,
1982
+ value: displayValue
1983
+ }), jsxRuntime.jsxs("div", {
1984
+ class: classNames__default['default']('fjs-number-arrow-container', {
1985
+ 'disabled': disabled
1986
+ }),
1987
+ children: [jsxRuntime.jsx("button", {
1988
+ class: "fjs-number-arrow-up",
1989
+ onClick: () => increment(),
1990
+ tabIndex: -1,
1991
+ children: "\u02C4"
1992
+ }), jsxRuntime.jsx("div", {
1993
+ class: "fjs-number-arrow-separator"
1994
+ }), jsxRuntime.jsx("button", {
1995
+ class: "fjs-number-arrow-down",
1996
+ onClick: () => decrement(),
1997
+ tabIndex: -1,
1998
+ children: "\u02C5"
1999
+ })]
2000
+ })]
1508
2001
  }), jsxRuntime.jsx(Description, {
1509
2002
  description: description
1510
2003
  }), jsxRuntime.jsx(Errors, {
@@ -1512,21 +2005,24 @@ function Number(props) {
1512
2005
  })]
1513
2006
  });
1514
2007
  }
1515
- Number.create = function (options = {}) {
1516
- return {
1517
- ...options
1518
- };
1519
- };
1520
- Number.sanitizeValue = ({
1521
- value
2008
+ Numberfield.create = (options = {}) => options;
2009
+ Numberfield.sanitizeValue = ({
2010
+ value,
2011
+ formField
1522
2012
  }) => {
1523
- const parsedValue = parseInt(value, 10);
1524
- return isNaN(parsedValue) ? null : parsedValue;
2013
+ // null state is allowed
2014
+ if (isNullEquivalentValue(value)) return null;
2015
+
2016
+ // if data cannot be parsed as a valid number, go into invalid NaN state
2017
+ if (!isValidNumber(value)) return 'NaN';
2018
+
2019
+ // otherwise parse to formatting type
2020
+ return formField.serializeToString ? value.toString() : Number(value);
1525
2021
  };
1526
- Number.type = type$6;
1527
- Number.keyed = true;
1528
- Number.label = 'Number';
1529
- Number.emptyValue = null;
2022
+ Numberfield.type = type$6;
2023
+ Numberfield.keyed = true;
2024
+ Numberfield.label = 'Number';
2025
+ Numberfield.emptyValue = null;
1530
2026
 
1531
2027
  const type$5 = 'radio';
1532
2028
  function Radio(props) {
@@ -1898,6 +2394,18 @@ function Taglist(props) {
1898
2394
  break;
1899
2395
  }
1900
2396
  };
2397
+ const onTagRemoveClick = (event, value) => {
2398
+ const {
2399
+ target
2400
+ } = event;
2401
+ deselectValue(value);
2402
+
2403
+ // restore focus if there is no next sibling to focus
2404
+ const nextTag = target.closest('.fjs-taglist-tag').nextSibling;
2405
+ if (!nextTag) {
2406
+ searchbarRef.current.focus();
2407
+ }
2408
+ };
1901
2409
  return jsxRuntime.jsxs("div", {
1902
2410
  class: formFieldClasses(type$3, {
1903
2411
  errors,
@@ -1910,19 +2418,24 @@ function Taglist(props) {
1910
2418
  class: classNames__default['default']('fjs-taglist', {
1911
2419
  'disabled': disabled
1912
2420
  }),
1913
- children: [!disabled && loadState === LOAD_STATES.LOADED && values.map(v => {
1914
- return jsxRuntime.jsxs("div", {
1915
- class: "fjs-taglist-tag",
1916
- onMouseDown: e => e.preventDefault(),
1917
- children: [jsxRuntime.jsx("span", {
1918
- class: "fjs-taglist-tag-label",
1919
- children: valueToOptionMap[v] ? valueToOptionMap[v].label : `unexpected value{${v}}`
1920
- }), jsxRuntime.jsx("span", {
1921
- class: "fjs-taglist-tag-remove",
1922
- onMouseDown: () => deselectValue(v),
1923
- children: jsxRuntime.jsx(CloseIcon, {})
1924
- })]
1925
- });
2421
+ children: [!disabled && loadState === LOAD_STATES.LOADED && jsxRuntime.jsx("div", {
2422
+ class: "fjs-taglist-tags",
2423
+ children: values.map(v => {
2424
+ return jsxRuntime.jsxs("div", {
2425
+ class: "fjs-taglist-tag",
2426
+ onMouseDown: e => e.preventDefault(),
2427
+ children: [jsxRuntime.jsx("span", {
2428
+ class: "fjs-taglist-tag-label",
2429
+ children: valueToOptionMap[v] ? valueToOptionMap[v].label : `unexpected value{${v}}`
2430
+ }), jsxRuntime.jsx("button", {
2431
+ type: "button",
2432
+ title: "Remove tag",
2433
+ class: "fjs-taglist-tag-remove",
2434
+ onClick: event => onTagRemoveClick(event, v),
2435
+ children: jsxRuntime.jsx(CloseIcon, {})
2436
+ })]
2437
+ });
2438
+ })
1926
2439
  }), jsxRuntime.jsx("input", {
1927
2440
  disabled: disabled,
1928
2441
  class: "fjs-taglist-input",
@@ -2143,7 +2656,7 @@ Textarea.sanitizeValue = ({
2143
2656
  value
2144
2657
  }) => minDash.isArray(value) || minDash.isObject(value) ? '' : String(value);
2145
2658
 
2146
- const formFields = [Button, Checkbox, Checklist, Default, Number, Radio, Select, Taglist, Text, Textfield, Textarea];
2659
+ const formFields = [Button, Checkbox, Checklist, Default, Image, Numberfield, Radio, Select, Taglist, Text, Textfield, Textarea];
2147
2660
 
2148
2661
  class FormFields {
2149
2662
  constructor() {
@@ -2223,6 +2736,7 @@ var renderModule = {
2223
2736
 
2224
2737
  var core = {
2225
2738
  __depends__: [importModule, renderModule],
2739
+ conditionChecker: ['type', ConditionChecker],
2226
2740
  eventBus: ['type', EventBus],
2227
2741
  formFieldRegistry: ['type', FormFieldRegistry],
2228
2742
  validator: ['type', Validator]
@@ -2338,14 +2852,14 @@ class Form {
2338
2852
  this.clear();
2339
2853
  const {
2340
2854
  schema: importedSchema,
2341
- data: importedData,
2855
+ data: initializedData,
2342
2856
  warnings
2343
2857
  } = this.get('importer').importSchema(schema, data);
2344
2858
  this._setState({
2345
- data: importedData,
2859
+ data: initializedData,
2346
2860
  errors: {},
2347
2861
  schema: importedSchema,
2348
- initialData: clone(importedData)
2862
+ initialData: clone(initializedData)
2349
2863
  });
2350
2864
  this._emit('import.done', {
2351
2865
  warnings
@@ -2377,14 +2891,13 @@ class Form {
2377
2891
  }
2378
2892
  const data = this._getSubmitData();
2379
2893
  const errors = this.validate();
2380
- this._emit('submit', {
2381
- data,
2382
- errors
2383
- });
2384
- return {
2894
+ const filteredErrors = this._applyConditions(errors, data);
2895
+ const result = {
2385
2896
  data,
2386
- errors
2897
+ errors: filteredErrors
2387
2898
  };
2899
+ this._emit('submit', result);
2900
+ return result;
2388
2901
  }
2389
2902
  reset() {
2390
2903
  this._emit('reset');
@@ -2565,7 +3078,8 @@ class Form {
2565
3078
  */
2566
3079
  _getSubmitData() {
2567
3080
  const formFieldRegistry = this.get('formFieldRegistry');
2568
- return formFieldRegistry.getAll().reduce((data, field) => {
3081
+ const formData = this._getState().data;
3082
+ const submitData = formFieldRegistry.getAll().reduce((previous, field) => {
2569
3083
  const {
2570
3084
  disabled,
2571
3085
  _path
@@ -2573,14 +3087,24 @@ class Form {
2573
3087
 
2574
3088
  // do not submit disabled form fields
2575
3089
  if (disabled || !_path) {
2576
- return data;
3090
+ return previous;
2577
3091
  }
2578
- const value = minDash.get(this._getState().data, _path);
3092
+ const value = minDash.get(formData, _path);
2579
3093
  return {
2580
- ...data,
3094
+ ...previous,
2581
3095
  [_path[0]]: value
2582
3096
  };
2583
3097
  }, {});
3098
+ const filteredSubmitData = this._applyConditions(submitData, formData);
3099
+ return filteredSubmitData;
3100
+ }
3101
+
3102
+ /**
3103
+ * @internal
3104
+ */
3105
+ _applyConditions(toFilter, data) {
3106
+ const conditionChecker = this.get('conditionChecker');
3107
+ return conditionChecker.applyConditions(toFilter, data);
2584
3108
  }
2585
3109
  }
2586
3110
 
@@ -2619,7 +3143,8 @@ exports.FormContext = FormContext;
2619
3143
  exports.FormFieldRegistry = FormFieldRegistry;
2620
3144
  exports.FormFields = FormFields;
2621
3145
  exports.FormRenderContext = FormRenderContext;
2622
- exports.Number = Number;
3146
+ exports.Image = Image;
3147
+ exports.Numberfield = Numberfield;
2623
3148
  exports.Radio = Radio;
2624
3149
  exports.Select = Select;
2625
3150
  exports.Taglist = Taglist;