@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.es.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import Ids from 'ids';
2
- import { isArray, isFunction, isNumber, bind, assign, isNil, get, isUndefined, isObject, set, isString } from 'min-dash';
2
+ import { isString, isArray, isFunction, isNumber, bind, assign, isNil, get, isUndefined, isObject, set } from 'min-dash';
3
+ import { unaryTest, evaluate, parseUnaryTests, parseExpressions } from 'feelin';
4
+ import Big from 'big.js';
3
5
  import snarkdown from '@bpmn-io/snarkdown';
4
6
  import classNames from 'classnames';
5
7
  import { jsx, jsxs } from 'preact/jsx-runtime';
@@ -9,6 +11,126 @@ import React, { createPortal } from 'preact/compat';
9
11
  import Markup from 'preact-markup';
10
12
  import { Injector } from 'didi';
11
13
 
14
+ /**
15
+ * @typedef {object} Condition
16
+ * @property {string} [hide]
17
+ */
18
+
19
+ class ConditionChecker {
20
+ constructor(formFieldRegistry, eventBus) {
21
+ this._formFieldRegistry = formFieldRegistry;
22
+ this._eventBus = eventBus;
23
+ }
24
+
25
+ /**
26
+ * For given data, remove properties based on condition.
27
+ *
28
+ * @param {Object<string, any>} properties
29
+ * @param {Object<string, any>} data
30
+ */
31
+ applyConditions(properties, data = {}) {
32
+ const conditions = this._getConditions();
33
+ const newProperties = {
34
+ ...properties
35
+ };
36
+ for (const {
37
+ key,
38
+ condition
39
+ } of conditions) {
40
+ const shouldRemove = this._checkHideCondition(condition, data);
41
+ if (shouldRemove) {
42
+ delete newProperties[key];
43
+ }
44
+ }
45
+ return newProperties;
46
+ }
47
+
48
+ /**
49
+ * Check if given condition is met. Returns null for invalid/missing conditions.
50
+ *
51
+ * @param {string} condition
52
+ * @param {import('../types').Data} [data]
53
+ *
54
+ * @returns {boolean|null}
55
+ */
56
+ check(condition, data = {}) {
57
+ if (!condition) {
58
+ return null;
59
+ }
60
+ if (!isString(condition) || !condition.startsWith('=')) {
61
+ return null;
62
+ }
63
+ try {
64
+ // cut off initial '='
65
+ const result = unaryTest(condition.slice(1), data);
66
+ return result;
67
+ } catch (error) {
68
+ this._eventBus.fire('error', {
69
+ error
70
+ });
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check if hide condition is met.
77
+ *
78
+ * @param {Condition} condition
79
+ * @param {Object<string, any>} data
80
+ * @returns {boolean}
81
+ */
82
+ _checkHideCondition(condition, data) {
83
+ if (!condition.hide) {
84
+ return false;
85
+ }
86
+ const result = this.check(condition.hide, data);
87
+ return result === true;
88
+ }
89
+
90
+ /**
91
+ * Evaluate an expression.
92
+ *
93
+ * @param {string} expression
94
+ * @param {import('../types').Data} [data]
95
+ *
96
+ * @returns {any}
97
+ */
98
+ evaluate(expression, data = {}) {
99
+ if (!expression) {
100
+ return null;
101
+ }
102
+ if (!isString(expression) || !expression.startsWith('=')) {
103
+ return null;
104
+ }
105
+ try {
106
+ const result = evaluate(expression.slice(1), data);
107
+ return result;
108
+ } catch (error) {
109
+ this._eventBus.fire('error', {
110
+ error
111
+ });
112
+ return null;
113
+ }
114
+ }
115
+ _getConditions() {
116
+ const formFields = this._formFieldRegistry.getAll();
117
+ return formFields.reduce((conditions, formField) => {
118
+ const {
119
+ key,
120
+ conditional: condition
121
+ } = formField;
122
+ if (key && condition) {
123
+ return [...conditions, {
124
+ key,
125
+ condition
126
+ }];
127
+ }
128
+ return conditions;
129
+ }, []);
130
+ }
131
+ }
132
+ ConditionChecker.$inject = ['formFieldRegistry', 'eventBus'];
133
+
12
134
  var FN_REF = '__fn';
13
135
  var DEFAULT_PRIORITY = 1000;
14
136
  var slice = Array.prototype.slice;
@@ -442,14 +564,63 @@ function invokeFunction(fn, args) {
442
564
  return fn.apply(null, args);
443
565
  }
444
566
 
567
+ function countDecimals(number) {
568
+ const num = Big(number);
569
+ if (num.toString() === num.toFixed(0)) return 0;
570
+ return num.toFixed().split('.')[1].length || 0;
571
+ }
572
+ function isValidNumber(value) {
573
+ return (typeof value === 'number' || typeof value === 'string') && value !== '' && !isNaN(Number(value));
574
+ }
575
+ function willKeyProduceValidNumber(key, previousValue, carretIndex, selectionWidth, decimalDigits) {
576
+ // Dot and comma are both treated as dot
577
+ previousValue = previousValue.replace(',', '.');
578
+ const isFirstDot = !previousValue.includes('.') && (key === '.' || key === ',');
579
+ const isFirstMinus = !previousValue.includes('-') && key === '-' && carretIndex === 0;
580
+ const keypressIsNumeric = /^[0-9]$/i.test(key);
581
+ const dotIndex = previousValue?.indexOf('.') ?? -1;
582
+
583
+ // 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
584
+ const overflowsDecimalSpace = typeof decimalDigits === 'number' && selectionWidth === 0 && dotIndex !== -1 && previousValue.includes('.') && previousValue.split('.')[1].length >= decimalDigits && carretIndex > dotIndex;
585
+ const keypressIsAllowedChar = keypressIsNumeric || decimalDigits !== 0 && isFirstDot || isFirstMinus;
586
+ return keypressIsAllowedChar && !overflowsDecimalSpace;
587
+ }
588
+ function isNullEquivalentValue(value) {
589
+ return value === undefined || value === null || value === '';
590
+ }
591
+
445
592
  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])?)*$/;
446
593
  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}$/;
447
594
  class Validator {
448
595
  validateField(field, value) {
449
596
  const {
597
+ type,
450
598
  validate
451
599
  } = field;
452
600
  let errors = [];
601
+ if (type === 'number') {
602
+ const {
603
+ decimalDigits,
604
+ step
605
+ } = field;
606
+ if (value === 'NaN') {
607
+ errors = [...errors, 'Value is not a number.'];
608
+ } else if (value) {
609
+ if (decimalDigits >= 0 && countDecimals(value) > decimalDigits) {
610
+ errors = [...errors, 'Value is expected to ' + (decimalDigits === 0 ? 'be an integer' : `have at most ${decimalDigits} decimal digit${decimalDigits > 1 ? 's' : ''}`) + '.'];
611
+ }
612
+ if (step) {
613
+ const bigValue = Big(value);
614
+ const bigStep = Big(step);
615
+ const offset = bigValue.mod(bigStep);
616
+ if (offset.cmp(0) !== 0) {
617
+ const previousValue = bigValue.minus(offset);
618
+ const nextValue = previousValue.plus(bigStep);
619
+ errors = [...errors, `Please select a valid value, the two nearest valid values are ${previousValue} and ${nextValue}.`];
620
+ }
621
+ }
622
+ }
623
+ }
453
624
  if (!validate) {
454
625
  return errors;
455
626
  }
@@ -531,6 +702,44 @@ class FormFieldRegistry {
531
702
  }
532
703
  FormFieldRegistry.$inject = ['eventBus'];
533
704
 
705
+ /**
706
+ * Retrieve variable names from given FEEL unary test.
707
+ *
708
+ * @param {string} unaryTest
709
+ * @returns {string[]}
710
+ */
711
+ function getVariableNames(unaryTest) {
712
+ const tree = parseUnaryTests(unaryTest);
713
+ const cursor = tree.cursor();
714
+ const variables = new Set();
715
+ do {
716
+ const node = cursor.node;
717
+ if (node.type.name === 'VariableName') {
718
+ variables.add(unaryTest.slice(node.from, node.to));
719
+ }
720
+ } while (cursor.next());
721
+ return Array.from(variables);
722
+ }
723
+
724
+ /**
725
+ * Retrieve variable names from given FEEL expression.
726
+ *
727
+ * @param {string} expression
728
+ * @returns {string[]}
729
+ */
730
+ function getExpressionVariableNames(expression) {
731
+ const tree = parseExpressions(expression);
732
+ const cursor = tree.cursor();
733
+ const variables = new Set();
734
+ do {
735
+ const node = cursor.node;
736
+ if (node.type.name === 'VariableName') {
737
+ variables.add(expression.slice(node.from, node.to));
738
+ }
739
+ } while (cursor.next());
740
+ return Array.from(variables);
741
+ }
742
+
534
743
  function createInjector(bootstrapModules) {
535
744
  const injector = new Injector(bootstrapModules);
536
745
  injector.init();
@@ -548,6 +757,7 @@ function createFormContainer(prefix = 'fjs') {
548
757
  return container;
549
758
  }
550
759
 
760
+ const EXPRESSION_PROPERTIES = ['alt', 'source'];
551
761
  function findErrors(errors, path) {
552
762
  return errors[pathStringify(path)];
553
763
  }
@@ -601,16 +811,16 @@ function clone(data, replacer) {
601
811
  *
602
812
  * @return {string[]}
603
813
  */
604
-
605
814
  function getSchemaVariables(schema) {
606
815
  if (!schema.components) {
607
816
  return [];
608
817
  }
609
- return schema.components.reduce((variables, component) => {
818
+ const variables = schema.components.reduce((variables, component) => {
610
819
  const {
611
820
  key,
612
821
  valuesKey,
613
- type
822
+ type,
823
+ conditional
614
824
  } = component;
615
825
  if (['text', 'button'].includes(type)) {
616
826
  return variables;
@@ -618,11 +828,33 @@ function getSchemaVariables(schema) {
618
828
  if (key) {
619
829
  variables = [...variables, key];
620
830
  }
621
- if (valuesKey && !variables.includes(valuesKey)) {
831
+ if (valuesKey) {
622
832
  variables = [...variables, valuesKey];
623
833
  }
834
+ if (conditional && conditional.hide) {
835
+ // cut off initial '='
836
+ const conditionVariables = getVariableNames(conditional.hide.slice(1));
837
+ variables = [...variables, ...conditionVariables];
838
+ }
839
+ EXPRESSION_PROPERTIES.forEach(prop => {
840
+ const property = component[prop];
841
+ if (property && isExpression$1(property)) {
842
+ // cut off initial '='
843
+ const expressionVariables = getExpressionVariableNames(property.slice(1));
844
+ variables = [...variables, ...expressionVariables];
845
+ }
846
+ });
624
847
  return variables;
625
848
  }, []);
849
+
850
+ // remove duplicates
851
+ return Array.from(new Set(variables));
852
+ }
853
+
854
+ // helper ///////////////
855
+
856
+ function isExpression$1(value) {
857
+ return isString(value) && value.startsWith('=');
626
858
  }
627
859
 
628
860
  class Importer {
@@ -651,11 +883,11 @@ class Importer {
651
883
  const warnings = [];
652
884
  try {
653
885
  const importedSchema = this.importFormField(clone(schema)),
654
- importedData = this.importData(clone(data));
886
+ initializedData = this.initializeFieldValues(clone(data));
655
887
  return {
656
888
  warnings,
657
889
  schema: importedSchema,
658
- data: importedData
890
+ data: initializedData
659
891
  };
660
892
  } catch (err) {
661
893
  err.warnings = warnings;
@@ -721,26 +953,16 @@ class Importer {
721
953
  /**
722
954
  * @param {Object} data
723
955
  *
724
- * @return {Object} importedData
956
+ * @return {Object} initializedData
725
957
  */
726
- importData(data) {
727
- return this._formFieldRegistry.getAll().reduce((importedData, formField) => {
958
+ initializeFieldValues(data) {
959
+ return this._formFieldRegistry.getAll().reduce((initializedData, formField) => {
728
960
  const {
729
961
  defaultValue,
730
962
  _path,
731
- type,
732
- valuesKey
963
+ type
733
964
  } = formField;
734
965
 
735
- // get values defined via valuesKey
736
-
737
- if (valuesKey) {
738
- importedData = {
739
- ...importedData,
740
- [valuesKey]: get(data, [valuesKey])
741
- };
742
- }
743
-
744
966
  // try to get value from data
745
967
  // if unavailable - try to get default value from form field
746
968
  // if unavailable - get empty value from form field
@@ -755,14 +977,14 @@ class Importer {
755
977
  value: valueData
756
978
  });
757
979
  }
758
- const initialFieldValue = !isUndefined(valueData) ? valueData : !isUndefined(defaultValue) ? defaultValue : fieldImplementation.emptyValue;
759
- importedData = {
760
- ...importedData,
761
- [_path[0]]: initialFieldValue
980
+ const initializedFieldValue = !isUndefined(valueData) ? valueData : !isUndefined(defaultValue) ? defaultValue : fieldImplementation.emptyValue;
981
+ initializedData = {
982
+ ...initializedData,
983
+ [_path[0]]: initializedFieldValue
762
984
  };
763
985
  }
764
- return importedData;
765
- }, {});
986
+ return initializedData;
987
+ }, data);
766
988
  }
767
989
  }
768
990
  Importer.$inject = ['formFieldRegistry', 'formFields'];
@@ -776,6 +998,7 @@ const NODE_TYPE_TEXT = 3,
776
998
  const ALLOWED_NODES = ['h1', 'h2', 'h3', 'h4', 'h5', 'span', 'em', 'a', 'p', 'div', 'ul', 'ol', 'li', 'hr', 'blockquote', 'img', 'pre', 'code', 'br', 'strong'];
777
999
  const ALLOWED_ATTRIBUTES = ['align', 'alt', 'class', 'href', 'id', 'name', 'rel', 'target', 'src'];
778
1000
  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
1001
+ const ALLOWED_IMAGE_SRC_PATTERN = /^(https?|data):.*/i; // eslint-disable-line no-useless-escape
779
1002
  const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
780
1003
 
781
1004
  const FORM_ELEMENT = document.createElement('form');
@@ -799,6 +1022,10 @@ function sanitizeHTML(html) {
799
1022
  return '';
800
1023
  }
801
1024
  }
1025
+ function sanitizeImageSource(src) {
1026
+ const valid = ALLOWED_IMAGE_SRC_PATTERN.test(src);
1027
+ return valid ? src : '';
1028
+ }
802
1029
 
803
1030
  /**
804
1031
  * Recursively sanitize a HTML node, potentially
@@ -909,6 +1136,19 @@ function safeMarkdown(markdown) {
909
1136
  const html = markdownToHTML(markdown);
910
1137
  return sanitizeHTML(html);
911
1138
  }
1139
+
1140
+ /**
1141
+ * Sanitizes an image source to ensure we only allow for data URI and links
1142
+ * that start with http(s).
1143
+ *
1144
+ * Note: Most browsers anyway do not support script execution in <img> elements.
1145
+ *
1146
+ * @param {string} src
1147
+ * @returns {string}
1148
+ */
1149
+ function safeImageSource(src) {
1150
+ return sanitizeImageSource(src);
1151
+ }
912
1152
  function sanitizeSingleSelectValue(options) {
913
1153
  const {
914
1154
  formField,
@@ -948,7 +1188,7 @@ function sanitizeMultiSelectValue(options) {
948
1188
  }
949
1189
  }
950
1190
 
951
- const type$9 = 'button';
1191
+ const type$a = 'button';
952
1192
  function Button(props) {
953
1193
  const {
954
1194
  disabled,
@@ -958,7 +1198,7 @@ function Button(props) {
958
1198
  action = 'submit'
959
1199
  } = field;
960
1200
  return jsx("div", {
961
- class: formFieldClasses(type$9),
1201
+ class: formFieldClasses(type$a),
962
1202
  children: jsx("button", {
963
1203
  class: "fjs-button",
964
1204
  type: action,
@@ -973,7 +1213,7 @@ Button.create = function (options = {}) {
973
1213
  ...options
974
1214
  };
975
1215
  };
976
- Button.type = type$9;
1216
+ Button.type = type$a;
977
1217
  Button.label = 'Button';
978
1218
  Button.keyed = true;
979
1219
 
@@ -1049,7 +1289,7 @@ function Label(props) {
1049
1289
  });
1050
1290
  }
1051
1291
 
1052
- const type$8 = 'checkbox';
1292
+ const type$9 = 'checkbox';
1053
1293
  function Checkbox(props) {
1054
1294
  const {
1055
1295
  disabled,
@@ -1074,7 +1314,7 @@ function Checkbox(props) {
1074
1314
  formId
1075
1315
  } = useContext(FormContext);
1076
1316
  return jsxs("div", {
1077
- class: classNames(formFieldClasses(type$8, {
1317
+ class: classNames(formFieldClasses(type$9, {
1078
1318
  errors,
1079
1319
  disabled
1080
1320
  }), {
@@ -1104,7 +1344,7 @@ Checkbox.create = function (options = {}) {
1104
1344
  ...options
1105
1345
  };
1106
1346
  };
1107
- Checkbox.type = type$8;
1347
+ Checkbox.type = type$9;
1108
1348
  Checkbox.label = 'Checkbox';
1109
1349
  Checkbox.keyed = true;
1110
1350
  Checkbox.emptyValue = false;
@@ -1179,7 +1419,7 @@ const buildLoadedState = values => ({
1179
1419
  state: LOAD_STATES.LOADED
1180
1420
  });
1181
1421
 
1182
- const type$7 = 'checklist';
1422
+ const type$8 = 'checklist';
1183
1423
  function Checklist(props) {
1184
1424
  const {
1185
1425
  disabled,
@@ -1212,7 +1452,7 @@ function Checklist(props) {
1212
1452
  formId
1213
1453
  } = useContext(FormContext);
1214
1454
  return jsxs("div", {
1215
- class: classNames(formFieldClasses(type$7, {
1455
+ class: classNames(formFieldClasses(type$8, {
1216
1456
  errors,
1217
1457
  disabled
1218
1458
  })),
@@ -1252,12 +1492,28 @@ Checklist.create = function (options = {}) {
1252
1492
  ...options
1253
1493
  };
1254
1494
  };
1255
- Checklist.type = type$7;
1495
+ Checklist.type = type$8;
1256
1496
  Checklist.label = 'Checklist';
1257
1497
  Checklist.keyed = true;
1258
1498
  Checklist.emptyValue = [];
1259
1499
  Checklist.sanitizeValue = sanitizeMultiSelectValue;
1260
1500
 
1501
+ /**
1502
+ * Check if condition is met with given variables.
1503
+ *
1504
+ * @param {string | undefined} condition
1505
+ * @param {import('../../types').Data} data
1506
+ *
1507
+ * @returns {boolean} true if condition is met or no condition or condition checker exists
1508
+ */
1509
+ function useCondition(condition, data) {
1510
+ const conditionChecker = useService('conditionChecker', false);
1511
+ if (!conditionChecker) {
1512
+ return null;
1513
+ }
1514
+ return conditionChecker.check(condition, data);
1515
+ }
1516
+
1261
1517
  const noop$1 = () => false;
1262
1518
  function FormField(props) {
1263
1519
  const {
@@ -1275,7 +1531,8 @@ function FormField(props) {
1275
1531
  properties
1276
1532
  } = form._getState();
1277
1533
  const {
1278
- Element
1534
+ Element,
1535
+ Empty
1279
1536
  } = useContext(FormRenderContext);
1280
1537
  const FormFieldComponent = formFields.get(field.type);
1281
1538
  if (!FormFieldComponent) {
@@ -1284,6 +1541,10 @@ function FormField(props) {
1284
1541
  const value = get(data, _path);
1285
1542
  const fieldErrors = findErrors(errors, _path);
1286
1543
  const disabled = properties.readOnly || field.disabled || false;
1544
+ const hidden = useHideCondition(field, data);
1545
+ if (hidden) {
1546
+ return jsx(Empty, {});
1547
+ }
1287
1548
  return jsx(Element, {
1288
1549
  field: field,
1289
1550
  children: jsx(FormFieldComponent, {
@@ -1295,6 +1556,10 @@ function FormField(props) {
1295
1556
  })
1296
1557
  });
1297
1558
  }
1559
+ function useHideCondition(field, data) {
1560
+ const hideCondition = field.conditional && field.conditional.hide;
1561
+ return useCondition(hideCondition, data) === true;
1562
+ }
1298
1563
 
1299
1564
  function Default(props) {
1300
1565
  const {
@@ -1440,6 +1705,7 @@ function FormComponent(props) {
1440
1705
  class: "fjs-form",
1441
1706
  onSubmit: handleSubmit,
1442
1707
  onReset: handleReset,
1708
+ noValidate: true,
1443
1709
  children: [jsx(FormField, {
1444
1710
  field: schema,
1445
1711
  onChange: onChange
@@ -1447,32 +1713,226 @@ function FormComponent(props) {
1447
1713
  });
1448
1714
  }
1449
1715
 
1716
+ /**
1717
+ *
1718
+ * @param {string | undefined} expression
1719
+ * @param {import('../../types').Data} data
1720
+ */
1721
+ function useEvaluation(expression, data) {
1722
+ const conditionChecker = useService('conditionChecker', false);
1723
+ if (!conditionChecker) {
1724
+ return null;
1725
+ }
1726
+ return conditionChecker.evaluate(expression, data);
1727
+ }
1728
+
1729
+ /**
1730
+ *
1731
+ * @param {string} value
1732
+ */
1733
+ function useExpressionValue(value) {
1734
+ const formData = useService('form')._getState().data;
1735
+ if (!isExpression(value)) {
1736
+ return value;
1737
+ }
1738
+
1739
+ // We can ignore this hook rule as we do not use
1740
+ // state or effects in our custom hooks
1741
+ /* eslint-disable-next-line react-hooks/rules-of-hooks */
1742
+ return useEvaluation(value, formData);
1743
+ }
1744
+
1745
+ // helper ///////////////
1746
+
1747
+ function isExpression(value) {
1748
+ return isString(value) && value.startsWith('=');
1749
+ }
1750
+
1751
+ 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); }
1752
+ var ImagePlaceholder = (({
1753
+ styles = {},
1754
+ ...props
1755
+ }) => /*#__PURE__*/React.createElement("svg", _extends$1({
1756
+ width: "64",
1757
+ height: "64",
1758
+ viewBox: "0 0 1280 1280",
1759
+ xmlns: "http://www.w3.org/2000/svg",
1760
+ fillRule: "evenodd",
1761
+ clipRule: "evenodd",
1762
+ strokeLinejoin: "round",
1763
+ strokeMiterlimit: "2"
1764
+ }, props), /*#__PURE__*/React.createElement("path", {
1765
+ fill: "#e5e9ed",
1766
+ d: "M0 0h1280v1280H0z"
1767
+ }), /*#__PURE__*/React.createElement("path", {
1768
+ d: "M910 410H370v470h540V410zm-57.333 57.333v355.334H427.333V467.333h425.334z",
1769
+ fill: "#cad3db"
1770
+ }), /*#__PURE__*/React.createElement("path", {
1771
+ d: "M810 770H480v-60l100-170 130 170 100-65v125z",
1772
+ fill: "#cad3db"
1773
+ }), /*#__PURE__*/React.createElement("circle", {
1774
+ cx: "750",
1775
+ cy: "550",
1776
+ r: "50",
1777
+ fill: "#cad3db",
1778
+ transform: "translate(10 10)"
1779
+ })));
1780
+
1781
+ const type$7 = 'image';
1782
+ function Image(props) {
1783
+ const {
1784
+ field
1785
+ } = props;
1786
+ const {
1787
+ alt,
1788
+ id,
1789
+ source
1790
+ } = field;
1791
+ const safeSource = safeImageSource(useExpressionValue(source));
1792
+ const altText = useExpressionValue(alt);
1793
+ const {
1794
+ formId
1795
+ } = useContext(FormContext);
1796
+ return jsx("div", {
1797
+ class: formFieldClasses(type$7),
1798
+ children: jsxs("div", {
1799
+ class: "fjs-image-container",
1800
+ children: [safeSource && jsx("img", {
1801
+ alt: altText,
1802
+ src: safeSource,
1803
+ class: "fjs-image",
1804
+ id: prefixId(id, formId)
1805
+ }), !safeSource && jsx("div", {
1806
+ class: "fjs-image-placeholder",
1807
+ children: jsx(ImagePlaceholder, {
1808
+ alt: "This is an image placeholder"
1809
+ })
1810
+ })]
1811
+ })
1812
+ });
1813
+ }
1814
+ Image.create = function (options = {}) {
1815
+ return {
1816
+ ...options
1817
+ };
1818
+ };
1819
+ Image.type = type$7;
1820
+ Image.keyed = false;
1821
+
1450
1822
  const type$6 = 'number';
1451
- function Number(props) {
1823
+ function Numberfield(props) {
1452
1824
  const {
1453
1825
  disabled,
1454
1826
  errors = [],
1455
1827
  field,
1456
- value
1828
+ value,
1829
+ onChange
1457
1830
  } = props;
1458
1831
  const {
1459
1832
  description,
1460
1833
  id,
1461
1834
  label,
1462
- validate = {}
1835
+ validate = {},
1836
+ decimalDigits,
1837
+ serializeToString = false,
1838
+ increment: incrementValue
1463
1839
  } = field;
1464
1840
  const {
1465
1841
  required
1466
1842
  } = validate;
1467
- const onChange = ({
1468
- target
1469
- }) => {
1470
- props.onChange({
1843
+ const inputRef = useRef();
1844
+ const [stringValueCache, setStringValueCache] = useState('');
1845
+
1846
+ // checks whether the value currently in the form data is practically different from the one in the input field cache
1847
+ // this allows us to guarantee the field always displays valid form data, but without auto-simplifying values like 1.000 to 1
1848
+ const cacheValueMatchesState = useMemo(() => Numberfield.sanitizeValue({
1849
+ value,
1850
+ formField: field
1851
+ }) === Numberfield.sanitizeValue({
1852
+ value: stringValueCache,
1853
+ formField: field
1854
+ }), [stringValueCache, value, field]);
1855
+ const displayValue = useMemo(() => {
1856
+ if (value === 'NaN') return 'NaN';
1857
+ return cacheValueMatchesState ? stringValueCache : value || value === 0 ? Big(value).toFixed() : '';
1858
+ }, [stringValueCache, value, cacheValueMatchesState]);
1859
+ const arrowIncrementValue = useMemo(() => {
1860
+ if (incrementValue) return Big(incrementValue);
1861
+ if (decimalDigits) return Big(`1e-${decimalDigits}`);
1862
+ return Big('1');
1863
+ }, [decimalDigits, incrementValue]);
1864
+ const setValue = useCallback(stringValue => {
1865
+ if (isNullEquivalentValue(stringValue)) {
1866
+ setStringValueCache('');
1867
+ onChange({
1868
+ field,
1869
+ value: null
1870
+ });
1871
+ return;
1872
+ }
1873
+
1874
+ // treat commas as dots
1875
+ stringValue = stringValue.replaceAll(',', '.');
1876
+ if (isNaN(Number(stringValue))) {
1877
+ setStringValueCache('NaN');
1878
+ onChange({
1879
+ field,
1880
+ value: 'NaN'
1881
+ });
1882
+ return;
1883
+ }
1884
+ setStringValueCache(stringValue);
1885
+ onChange({
1471
1886
  field,
1472
- value: Number.sanitizeValue({
1473
- value: target.value
1474
- })
1887
+ value: serializeToString ? stringValue : Number(stringValue)
1475
1888
  });
1889
+ }, [field, onChange, serializeToString]);
1890
+ const increment = () => {
1891
+ const base = isValidNumber(value) ? Big(value) : Big(0);
1892
+ const stepFlooredValue = base.minus(base.mod(arrowIncrementValue));
1893
+
1894
+ // note: toFixed() behaves differently in big.js
1895
+ setValue(stepFlooredValue.plus(arrowIncrementValue).toFixed());
1896
+ };
1897
+ const decrement = () => {
1898
+ const base = isValidNumber(value) ? Big(value) : Big(0);
1899
+ const offset = base.mod(arrowIncrementValue);
1900
+ if (offset.cmp(0) === 0) {
1901
+ // if we're already on a valid step, decrement
1902
+ setValue(base.minus(arrowIncrementValue).toFixed());
1903
+ } else {
1904
+ // otherwise floor to the step
1905
+ const stepFlooredValue = base.minus(base.mod(arrowIncrementValue));
1906
+ setValue(stepFlooredValue.toFixed());
1907
+ }
1908
+ };
1909
+ const onKeyDown = e => {
1910
+ // delete the NaN state all at once on backspace or delete
1911
+ if (value === 'NaN' && (e.code === 'Backspace' || e.code === 'Delete')) {
1912
+ setValue(null);
1913
+ e.preventDefault();
1914
+ return;
1915
+ }
1916
+ if (e.code === 'ArrowUp') {
1917
+ increment();
1918
+ e.preventDefault();
1919
+ return;
1920
+ }
1921
+ if (e.code === 'ArrowDown') {
1922
+ decrement();
1923
+ e.preventDefault();
1924
+ return;
1925
+ }
1926
+ };
1927
+
1928
+ // intercept key presses which would lead to an invalid number
1929
+ const onKeyPress = e => {
1930
+ const carretIndex = inputRef.current.selectionStart;
1931
+ const selectionWidth = inputRef.current.selectionStart - inputRef.current.selectionEnd;
1932
+ const previousValue = inputRef.current.value;
1933
+ if (!willKeyProduceValidNumber(e.key, previousValue, carretIndex, selectionWidth, decimalDigits)) {
1934
+ e.preventDefault();
1935
+ }
1476
1936
  };
1477
1937
  const {
1478
1938
  formId
@@ -1486,13 +1946,45 @@ function Number(props) {
1486
1946
  id: prefixId(id, formId),
1487
1947
  label: label,
1488
1948
  required: required
1489
- }), jsx("input", {
1490
- class: "fjs-input",
1491
- disabled: disabled,
1492
- id: prefixId(id, formId),
1493
- onInput: onChange,
1494
- type: "number",
1495
- value: value || ''
1949
+ }), jsxs("div", {
1950
+ class: classNames('fjs-input-group', {
1951
+ 'disabled': disabled
1952
+ }, {
1953
+ 'hasErrors': errors.length
1954
+ }),
1955
+ children: [jsx("input", {
1956
+ ref: inputRef,
1957
+ class: "fjs-input",
1958
+ disabled: disabled,
1959
+ id: prefixId(id, formId),
1960
+ onKeyDown: onKeyDown,
1961
+ onKeyPress: onKeyPress
1962
+
1963
+ // @ts-ignore
1964
+ ,
1965
+ onInput: e => setValue(e.target.value),
1966
+ type: "text",
1967
+ autoComplete: "off",
1968
+ step: arrowIncrementValue,
1969
+ value: displayValue
1970
+ }), jsxs("div", {
1971
+ class: classNames('fjs-number-arrow-container', {
1972
+ 'disabled': disabled
1973
+ }),
1974
+ children: [jsx("button", {
1975
+ class: "fjs-number-arrow-up",
1976
+ onClick: () => increment(),
1977
+ tabIndex: -1,
1978
+ children: "\u02C4"
1979
+ }), jsx("div", {
1980
+ class: "fjs-number-arrow-separator"
1981
+ }), jsx("button", {
1982
+ class: "fjs-number-arrow-down",
1983
+ onClick: () => decrement(),
1984
+ tabIndex: -1,
1985
+ children: "\u02C5"
1986
+ })]
1987
+ })]
1496
1988
  }), jsx(Description, {
1497
1989
  description: description
1498
1990
  }), jsx(Errors, {
@@ -1500,21 +1992,24 @@ function Number(props) {
1500
1992
  })]
1501
1993
  });
1502
1994
  }
1503
- Number.create = function (options = {}) {
1504
- return {
1505
- ...options
1506
- };
1507
- };
1508
- Number.sanitizeValue = ({
1509
- value
1995
+ Numberfield.create = (options = {}) => options;
1996
+ Numberfield.sanitizeValue = ({
1997
+ value,
1998
+ formField
1510
1999
  }) => {
1511
- const parsedValue = parseInt(value, 10);
1512
- return isNaN(parsedValue) ? null : parsedValue;
2000
+ // null state is allowed
2001
+ if (isNullEquivalentValue(value)) return null;
2002
+
2003
+ // if data cannot be parsed as a valid number, go into invalid NaN state
2004
+ if (!isValidNumber(value)) return 'NaN';
2005
+
2006
+ // otherwise parse to formatting type
2007
+ return formField.serializeToString ? value.toString() : Number(value);
1513
2008
  };
1514
- Number.type = type$6;
1515
- Number.keyed = true;
1516
- Number.label = 'Number';
1517
- Number.emptyValue = null;
2009
+ Numberfield.type = type$6;
2010
+ Numberfield.keyed = true;
2011
+ Numberfield.label = 'Number';
2012
+ Numberfield.emptyValue = null;
1518
2013
 
1519
2014
  const type$5 = 'radio';
1520
2015
  function Radio(props) {
@@ -1886,6 +2381,18 @@ function Taglist(props) {
1886
2381
  break;
1887
2382
  }
1888
2383
  };
2384
+ const onTagRemoveClick = (event, value) => {
2385
+ const {
2386
+ target
2387
+ } = event;
2388
+ deselectValue(value);
2389
+
2390
+ // restore focus if there is no next sibling to focus
2391
+ const nextTag = target.closest('.fjs-taglist-tag').nextSibling;
2392
+ if (!nextTag) {
2393
+ searchbarRef.current.focus();
2394
+ }
2395
+ };
1889
2396
  return jsxs("div", {
1890
2397
  class: formFieldClasses(type$3, {
1891
2398
  errors,
@@ -1898,19 +2405,24 @@ function Taglist(props) {
1898
2405
  class: classNames('fjs-taglist', {
1899
2406
  'disabled': disabled
1900
2407
  }),
1901
- children: [!disabled && loadState === LOAD_STATES.LOADED && values.map(v => {
1902
- return jsxs("div", {
1903
- class: "fjs-taglist-tag",
1904
- onMouseDown: e => e.preventDefault(),
1905
- children: [jsx("span", {
1906
- class: "fjs-taglist-tag-label",
1907
- children: valueToOptionMap[v] ? valueToOptionMap[v].label : `unexpected value{${v}}`
1908
- }), jsx("span", {
1909
- class: "fjs-taglist-tag-remove",
1910
- onMouseDown: () => deselectValue(v),
1911
- children: jsx(CloseIcon, {})
1912
- })]
1913
- });
2408
+ children: [!disabled && loadState === LOAD_STATES.LOADED && jsx("div", {
2409
+ class: "fjs-taglist-tags",
2410
+ children: values.map(v => {
2411
+ return jsxs("div", {
2412
+ class: "fjs-taglist-tag",
2413
+ onMouseDown: e => e.preventDefault(),
2414
+ children: [jsx("span", {
2415
+ class: "fjs-taglist-tag-label",
2416
+ children: valueToOptionMap[v] ? valueToOptionMap[v].label : `unexpected value{${v}}`
2417
+ }), jsx("button", {
2418
+ type: "button",
2419
+ title: "Remove tag",
2420
+ class: "fjs-taglist-tag-remove",
2421
+ onClick: event => onTagRemoveClick(event, v),
2422
+ children: jsx(CloseIcon, {})
2423
+ })]
2424
+ });
2425
+ })
1914
2426
  }), jsx("input", {
1915
2427
  disabled: disabled,
1916
2428
  class: "fjs-taglist-input",
@@ -2131,7 +2643,7 @@ Textarea.sanitizeValue = ({
2131
2643
  value
2132
2644
  }) => isArray(value) || isObject(value) ? '' : String(value);
2133
2645
 
2134
- const formFields = [Button, Checkbox, Checklist, Default, Number, Radio, Select, Taglist, Text, Textfield, Textarea];
2646
+ const formFields = [Button, Checkbox, Checklist, Default, Image, Numberfield, Radio, Select, Taglist, Text, Textfield, Textarea];
2135
2647
 
2136
2648
  class FormFields {
2137
2649
  constructor() {
@@ -2211,6 +2723,7 @@ var renderModule = {
2211
2723
 
2212
2724
  var core = {
2213
2725
  __depends__: [importModule, renderModule],
2726
+ conditionChecker: ['type', ConditionChecker],
2214
2727
  eventBus: ['type', EventBus],
2215
2728
  formFieldRegistry: ['type', FormFieldRegistry],
2216
2729
  validator: ['type', Validator]
@@ -2326,14 +2839,14 @@ class Form {
2326
2839
  this.clear();
2327
2840
  const {
2328
2841
  schema: importedSchema,
2329
- data: importedData,
2842
+ data: initializedData,
2330
2843
  warnings
2331
2844
  } = this.get('importer').importSchema(schema, data);
2332
2845
  this._setState({
2333
- data: importedData,
2846
+ data: initializedData,
2334
2847
  errors: {},
2335
2848
  schema: importedSchema,
2336
- initialData: clone(importedData)
2849
+ initialData: clone(initializedData)
2337
2850
  });
2338
2851
  this._emit('import.done', {
2339
2852
  warnings
@@ -2365,14 +2878,13 @@ class Form {
2365
2878
  }
2366
2879
  const data = this._getSubmitData();
2367
2880
  const errors = this.validate();
2368
- this._emit('submit', {
2369
- data,
2370
- errors
2371
- });
2372
- return {
2881
+ const filteredErrors = this._applyConditions(errors, data);
2882
+ const result = {
2373
2883
  data,
2374
- errors
2884
+ errors: filteredErrors
2375
2885
  };
2886
+ this._emit('submit', result);
2887
+ return result;
2376
2888
  }
2377
2889
  reset() {
2378
2890
  this._emit('reset');
@@ -2553,7 +3065,8 @@ class Form {
2553
3065
  */
2554
3066
  _getSubmitData() {
2555
3067
  const formFieldRegistry = this.get('formFieldRegistry');
2556
- return formFieldRegistry.getAll().reduce((data, field) => {
3068
+ const formData = this._getState().data;
3069
+ const submitData = formFieldRegistry.getAll().reduce((previous, field) => {
2557
3070
  const {
2558
3071
  disabled,
2559
3072
  _path
@@ -2561,14 +3074,24 @@ class Form {
2561
3074
 
2562
3075
  // do not submit disabled form fields
2563
3076
  if (disabled || !_path) {
2564
- return data;
3077
+ return previous;
2565
3078
  }
2566
- const value = get(this._getState().data, _path);
3079
+ const value = get(formData, _path);
2567
3080
  return {
2568
- ...data,
3081
+ ...previous,
2569
3082
  [_path[0]]: value
2570
3083
  };
2571
3084
  }, {});
3085
+ const filteredSubmitData = this._applyConditions(submitData, formData);
3086
+ return filteredSubmitData;
3087
+ }
3088
+
3089
+ /**
3090
+ * @internal
3091
+ */
3092
+ _applyConditions(toFilter, data) {
3093
+ const conditionChecker = this.get('conditionChecker');
3094
+ return conditionChecker.applyConditions(toFilter, data);
2572
3095
  }
2573
3096
  }
2574
3097
 
@@ -2597,5 +3120,5 @@ function createForm(options) {
2597
3120
  });
2598
3121
  }
2599
3122
 
2600
- export { Button, Checkbox, Checklist, Default, Form, FormComponent, FormContext, FormFieldRegistry, FormFields, FormRenderContext, Number, Radio, Select, Taglist, Text, Textarea, Textfield, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getSchemaVariables, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
3123
+ export { Button, Checkbox, Checklist, Default, Form, FormComponent, FormContext, FormFieldRegistry, FormFields, FormRenderContext, Image, Numberfield, Radio, Select, Taglist, Text, Textarea, Textfield, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getSchemaVariables, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
2601
3124
  //# sourceMappingURL=index.es.js.map