@bpmn-io/form-js-viewer 0.7.2 → 0.8.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 (49) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +158 -158
  3. package/dist/assets/form-js.css +339 -238
  4. package/dist/index.cjs +688 -145
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +685 -147
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/types/Form.d.ts +136 -136
  9. package/dist/types/core/EventBus.d.ts +1 -1
  10. package/dist/types/core/FormFieldRegistry.d.ts +17 -17
  11. package/dist/types/core/Validator.d.ts +7 -7
  12. package/dist/types/core/index.d.ts +16 -16
  13. package/dist/types/import/Importer.d.ts +43 -43
  14. package/dist/types/import/index.d.ts +5 -5
  15. package/dist/types/index.d.ts +18 -18
  16. package/dist/types/render/FormFields.d.ts +5 -5
  17. package/dist/types/render/Renderer.d.ts +23 -23
  18. package/dist/types/render/components/Description.d.ts +1 -1
  19. package/dist/types/render/components/Errors.d.ts +1 -1
  20. package/dist/types/render/components/FormComponent.d.ts +1 -1
  21. package/dist/types/render/components/FormField.d.ts +1 -1
  22. package/dist/types/render/components/Label.d.ts +1 -1
  23. package/dist/types/render/components/PoweredBy.d.ts +1 -1
  24. package/dist/types/render/components/Sanitizer.d.ts +7 -7
  25. package/dist/types/render/components/Util.d.ts +6 -4
  26. package/dist/types/render/components/form-fields/Button.d.ts +11 -11
  27. package/dist/types/render/components/form-fields/Checkbox.d.ts +13 -10
  28. package/dist/types/render/components/form-fields/Checklist.d.ts +12 -0
  29. package/dist/types/render/components/form-fields/Default.d.ts +9 -9
  30. package/dist/types/render/components/form-fields/Number.d.ts +13 -10
  31. package/dist/types/render/components/form-fields/Radio.d.ts +12 -15
  32. package/dist/types/render/components/form-fields/Select.d.ts +12 -15
  33. package/dist/types/render/components/form-fields/Taglist.d.ts +12 -0
  34. package/dist/types/render/components/form-fields/Text.d.ts +10 -10
  35. package/dist/types/render/components/form-fields/Textfield.d.ts +13 -10
  36. package/dist/types/render/components/form-fields/parts/DropdownList.d.ts +1 -0
  37. package/dist/types/render/components/index.d.ts +13 -11
  38. package/dist/types/render/context/FormContext.d.ts +12 -12
  39. package/dist/types/render/context/FormRenderContext.d.ts +6 -6
  40. package/dist/types/render/context/index.d.ts +2 -2
  41. package/dist/types/render/hooks/useKeyDownAction.d.ts +1 -0
  42. package/dist/types/render/hooks/useService.d.ts +1 -1
  43. package/dist/types/render/hooks/useValuesAsync.d.ts +28 -0
  44. package/dist/types/render/index.d.ts +11 -11
  45. package/dist/types/types.d.ts +35 -35
  46. package/dist/types/util/form.d.ts +6 -6
  47. package/dist/types/util/index.d.ts +24 -16
  48. package/dist/types/util/injector.d.ts +2 -1
  49. package/package.json +6 -5
package/dist/index.cjs CHANGED
@@ -8,14 +8,17 @@ var snarkdown = require('@bpmn-io/snarkdown');
8
8
  var jsxRuntime = require('preact/jsx-runtime');
9
9
  var hooks = require('preact/hooks');
10
10
  var preact = require('preact');
11
+ var React = require('preact/compat');
12
+ var classNames = require('classnames');
11
13
  var Markup = require('preact-markup');
12
- var compat = require('preact/compat');
13
14
  var didi = require('didi');
14
15
 
15
16
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
16
17
 
17
18
  var Ids__default = /*#__PURE__*/_interopDefaultLegacy(Ids);
18
19
  var snarkdown__default = /*#__PURE__*/_interopDefaultLegacy(snarkdown);
20
+ var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
21
+ var classNames__default = /*#__PURE__*/_interopDefaultLegacy(classNames);
19
22
  var Markup__default = /*#__PURE__*/_interopDefaultLegacy(Markup);
20
23
 
21
24
  var FN_REF = '__fn';
@@ -598,52 +601,15 @@ class FormFieldRegistry {
598
601
  FormFieldRegistry.$inject = ['eventBus'];
599
602
 
600
603
  function createInjector(bootstrapModules) {
601
- const modules = [],
602
- components = [];
603
-
604
- function hasModule(module) {
605
- return modules.includes(module);
606
- }
607
-
608
- function addModule(module) {
609
- modules.push(module);
610
- }
611
-
612
- function visit(module) {
613
- if (hasModule(module)) {
614
- return;
615
- }
616
-
617
- (module.__depends__ || []).forEach(visit);
618
-
619
- if (hasModule(module)) {
620
- return;
621
- }
622
-
623
- addModule(module);
624
- (module.__init__ || []).forEach(function (component) {
625
- components.push(component);
626
- });
627
- }
628
-
629
- bootstrapModules.forEach(visit);
630
- const injector = new didi.Injector(modules);
631
- components.forEach(function (component) {
632
- try {
633
- injector[typeof component === 'string' ? 'get' : 'invoke'](component);
634
- } catch (err) {
635
- console.error('Failed to instantiate component');
636
- console.error(err.stack);
637
- throw err;
638
- }
639
- });
604
+ const injector = new didi.Injector(bootstrapModules);
605
+ injector.init();
640
606
  return injector;
641
607
  }
642
608
 
643
- /**
644
- * @param {string?} prefix
645
- *
646
- * @returns Element
609
+ /**
610
+ * @param {string?} prefix
611
+ *
612
+ * @returns Element
647
613
  */
648
614
  function createFormContainer(prefix = 'fjs') {
649
615
  const container = document.createElement('div');
@@ -689,41 +655,76 @@ function generateIndexForType(type) {
689
655
  function generateIdForType(type) {
690
656
  return `${type}${generateIndexForType(type)}`;
691
657
  }
692
- /**
693
- * @template T
694
- * @param {T} data
695
- * @param {(this: any, key: string, value: any) => any} [replacer]
696
- * @return {T}
658
+ /**
659
+ * @template T
660
+ * @param {T} data
661
+ * @param {(this: any, key: string, value: any) => any} [replacer]
662
+ * @return {T}
697
663
  */
698
664
 
699
665
  function clone(data, replacer) {
700
666
  return JSON.parse(JSON.stringify(data, replacer));
701
667
  }
668
+ /**
669
+ * Parse the schema for input variables a form might make use of
670
+ *
671
+ * @param {any} schema
672
+ *
673
+ * @return {string[]}
674
+ */
675
+
676
+ function getSchemaVariables(schema) {
677
+ if (!schema.components) {
678
+ return [];
679
+ }
680
+
681
+ return schema.components.reduce((variables, component) => {
682
+ const {
683
+ key,
684
+ valuesKey,
685
+ type
686
+ } = component;
687
+
688
+ if (['text', 'button'].includes(type)) {
689
+ return variables;
690
+ }
691
+
692
+ if (key) {
693
+ variables = [...variables, key];
694
+ }
695
+
696
+ if (valuesKey && !variables.includes(valuesKey)) {
697
+ variables = [...variables, valuesKey];
698
+ }
699
+
700
+ return variables;
701
+ }, []);
702
+ }
702
703
 
703
704
  class Importer {
704
- /**
705
- * @constructor
706
- * @param { import('../core').FormFieldRegistry } formFieldRegistry
707
- * @param { import('../render/FormFields').default } formFields
705
+ /**
706
+ * @constructor
707
+ * @param { import('../core').FormFieldRegistry } formFieldRegistry
708
+ * @param { import('../render/FormFields').default } formFields
708
709
  */
709
710
  constructor(formFieldRegistry, formFields) {
710
711
  this._formFieldRegistry = formFieldRegistry;
711
712
  this._formFields = formFields;
712
713
  }
713
- /**
714
- * Import schema adding `id`, `_parent` and `_path`
715
- * information to each field and adding it to the
716
- * form field registry.
717
- *
718
- * @param {any} schema
719
- * @param {any} [data]
720
- *
721
- * @return { { warnings: Array<any>, schema: any, data: any } }
714
+ /**
715
+ * Import schema adding `id`, `_parent` and `_path`
716
+ * information to each field and adding it to the
717
+ * form field registry.
718
+ *
719
+ * @param {any} schema
720
+ * @param {any} [data]
721
+ *
722
+ * @return { { warnings: Array<any>, schema: any, data: any } }
722
723
  */
723
724
 
724
725
 
725
726
  importSchema(schema, data = {}) {
726
- // TODO: Add warnings
727
+ // TODO: Add warnings - https://github.com/bpmn-io/form-js/issues/289
727
728
  const warnings = [];
728
729
 
729
730
  try {
@@ -739,11 +740,11 @@ class Importer {
739
740
  throw err;
740
741
  }
741
742
  }
742
- /**
743
- * @param {any} formField
744
- * @param {string} [parentId]
745
- *
746
- * @return {any} importedField
743
+ /**
744
+ * @param {any} formField
745
+ * @param {string} [parentId]
746
+ *
747
+ * @return {any} importedField
747
748
  */
748
749
 
749
750
 
@@ -805,10 +806,10 @@ class Importer {
805
806
  this.importFormField(component, parentId);
806
807
  });
807
808
  }
808
- /**
809
- * @param {Object} data
810
- *
811
- * @return {Object} importedData
809
+ /**
810
+ * @param {Object} data
811
+ *
812
+ * @return {Object} importedData
812
813
  */
813
814
 
814
815
 
@@ -817,19 +818,39 @@ class Importer {
817
818
  const {
818
819
  defaultValue,
819
820
  _path,
820
- type
821
- } = formField;
822
-
823
- if (!_path) {
824
- return importedData;
825
- } // (1) try to get value from data
826
- // (2) try to get default value from form field
827
- // (3) get empty value from form field
828
-
821
+ type,
822
+ valuesKey
823
+ } = formField; // get values defined via valuesKey
824
+
825
+ if (valuesKey) {
826
+ importedData = { ...importedData,
827
+ [valuesKey]: minDash.get(data, [valuesKey])
828
+ };
829
+ } // try to get value from data
830
+ // if unavailable - try to get default value from form field
831
+ // if unavailable - get empty value from form field
832
+
833
+
834
+ if (_path) {
835
+ const fieldImplementation = this._formFields.get(type);
836
+
837
+ let valueData = minDash.get(data, _path);
838
+
839
+ if (!minDash.isUndefined(valueData) && fieldImplementation.sanitizeValue) {
840
+ valueData = fieldImplementation.sanitizeValue({
841
+ formField,
842
+ data,
843
+ value: valueData
844
+ });
845
+ }
846
+
847
+ const initialFieldValue = !minDash.isUndefined(valueData) ? valueData : !minDash.isUndefined(defaultValue) ? defaultValue : fieldImplementation.emptyValue;
848
+ importedData = { ...importedData,
849
+ [_path[0]]: initialFieldValue
850
+ };
851
+ }
829
852
 
830
- return { ...importedData,
831
- [_path[0]]: minDash.get(data, _path, minDash.isUndefined(defaultValue) ? this._formFields.get(type).emptyValue : defaultValue)
832
- };
853
+ return importedData;
833
854
  }, {});
834
855
  }
835
856
 
@@ -849,11 +870,11 @@ const ALLOWED_URI_PATTERN = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^
849
870
  const ATTR_WHITESPACE_PATTERN = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g; // eslint-disable-line no-control-regex
850
871
 
851
872
  const FORM_ELEMENT = document.createElement('form');
852
- /**
853
- * Sanitize a HTML string and return the cleaned, safe version.
854
- *
855
- * @param {string} html
856
- * @return {string}
873
+ /**
874
+ * Sanitize a HTML string and return the cleaned, safe version.
875
+ *
876
+ * @param {string} html
877
+ * @return {string}
857
878
  */
858
879
 
859
880
  function sanitizeHTML(html) {
@@ -872,15 +893,15 @@ function sanitizeHTML(html) {
872
893
  return '';
873
894
  }
874
895
  }
875
- /**
876
- * Recursively sanitize a HTML node, potentially
877
- * removing it, its children or attributes.
878
- *
879
- * Inspired by https://github.com/developit/snarkdown/issues/70
880
- * and https://github.com/cure53/DOMPurify. Simplified
881
- * for our use-case.
882
- *
883
- * @param {Element} node
896
+ /**
897
+ * Recursively sanitize a HTML node, potentially
898
+ * removing it, its children or attributes.
899
+ *
900
+ * Inspired by https://github.com/developit/snarkdown/issues/70
901
+ * and https://github.com/cure53/DOMPurify. Simplified
902
+ * for our use-case.
903
+ *
904
+ * @param {Element} node
884
905
  */
885
906
 
886
907
  function sanitizeNode(node) {
@@ -927,13 +948,13 @@ function sanitizeNode(node) {
927
948
  node.childNodes[i]);
928
949
  }
929
950
  }
930
- /**
931
- * Validates attributes for validity.
932
- *
933
- * @param {string} lcTag
934
- * @param {string} lcName
935
- * @param {string} value
936
- * @return {boolean}
951
+ /**
952
+ * Validates attributes for validity.
953
+ *
954
+ * @param {string} lcTag
955
+ * @param {string} lcName
956
+ * @param {string} value
957
+ * @return {boolean}
937
958
  */
938
959
 
939
960
 
@@ -989,8 +1010,48 @@ function safeMarkdown(markdown) {
989
1010
  const html = markdownToHTML(markdown);
990
1011
  return sanitizeHTML(html);
991
1012
  }
1013
+ function sanitizeSingleSelectValue(options) {
1014
+ const {
1015
+ formField,
1016
+ data,
1017
+ value
1018
+ } = options;
1019
+ const {
1020
+ valuesKey,
1021
+ values
1022
+ } = formField;
1023
+
1024
+ try {
1025
+ const validValues = (valuesKey ? minDash.get(data, [valuesKey]) : values).map(v => v.value) || [];
1026
+ return validValues.includes(value) ? value : null;
1027
+ } catch (error) {
1028
+ // use default value in case of formatting error
1029
+ // TODO(@Skaiir): log a warning when this happens - https://github.com/bpmn-io/form-js/issues/289
1030
+ return null;
1031
+ }
1032
+ }
1033
+ function sanitizeMultiSelectValue(options) {
1034
+ const {
1035
+ formField,
1036
+ data,
1037
+ value
1038
+ } = options;
1039
+ const {
1040
+ valuesKey,
1041
+ values
1042
+ } = formField;
1043
+
1044
+ try {
1045
+ const validValues = (valuesKey ? minDash.get(data, [valuesKey]) : values).map(v => v.value) || [];
1046
+ return value.filter(v => validValues.includes(v));
1047
+ } catch (error) {
1048
+ // use default value in case of formatting error
1049
+ // TODO(@Skaiir): log a warning when this happens - https://github.com/bpmn-io/form-js/issues/289
1050
+ return [];
1051
+ }
1052
+ }
992
1053
 
993
- const type$6 = 'button';
1054
+ const type$8 = 'button';
994
1055
  function Button(props) {
995
1056
  const {
996
1057
  disabled,
@@ -1000,7 +1061,7 @@ function Button(props) {
1000
1061
  action = 'submit'
1001
1062
  } = field;
1002
1063
  return jsxRuntime.jsx("div", {
1003
- class: formFieldClasses(type$6),
1064
+ class: formFieldClasses(type$8),
1004
1065
  children: jsxRuntime.jsx("button", {
1005
1066
  class: "fjs-button",
1006
1067
  type: action,
@@ -1017,7 +1078,7 @@ Button.create = function (options = {}) {
1017
1078
  };
1018
1079
  };
1019
1080
 
1020
- Button.type = type$6;
1081
+ Button.type = type$8;
1021
1082
  Button.label = 'Button';
1022
1083
  Button.keyed = true;
1023
1084
 
@@ -1033,11 +1094,11 @@ const FormRenderContext = preact.createContext({
1033
1094
  }
1034
1095
  });
1035
1096
 
1036
- /**
1037
- * @param {string} type
1038
- * @param {boolean} [strict]
1039
- *
1040
- * @returns {any}
1097
+ /**
1098
+ * @param {string} type
1099
+ * @param {boolean} [strict]
1100
+ *
1101
+ * @returns {any}
1041
1102
  */
1042
1103
 
1043
1104
  function getService(type, strict) {}
@@ -1099,7 +1160,7 @@ function Label(props) {
1099
1160
  });
1100
1161
  }
1101
1162
 
1102
- const type$5 = 'checkbox';
1163
+ const type$7 = 'checkbox';
1103
1164
  function Checkbox(props) {
1104
1165
  const {
1105
1166
  disabled,
@@ -1126,7 +1187,7 @@ function Checkbox(props) {
1126
1187
  formId
1127
1188
  } = hooks.useContext(FormContext);
1128
1189
  return jsxRuntime.jsxs("div", {
1129
- class: formFieldClasses(type$5, errors),
1190
+ class: formFieldClasses(type$7, errors),
1130
1191
  children: [jsxRuntime.jsx(Label, {
1131
1192
  id: prefixId(id, formId),
1132
1193
  label: label,
@@ -1152,11 +1213,15 @@ Checkbox.create = function (options = {}) {
1152
1213
  };
1153
1214
  };
1154
1215
 
1155
- Checkbox.type = type$5;
1216
+ Checkbox.type = type$7;
1156
1217
  Checkbox.label = 'Checkbox';
1157
1218
  Checkbox.keyed = true;
1158
1219
  Checkbox.emptyValue = false;
1159
1220
 
1221
+ Checkbox.sanitizeValue = ({
1222
+ value
1223
+ }) => value === true;
1224
+
1160
1225
  function useService (type, strict) {
1161
1226
  const {
1162
1227
  getService
@@ -1164,6 +1229,153 @@ function useService (type, strict) {
1164
1229
  return getService(type, strict);
1165
1230
  }
1166
1231
 
1232
+ /**
1233
+ * @enum { String }
1234
+ */
1235
+
1236
+ const LOAD_STATES = {
1237
+ LOADING: 'loading',
1238
+ LOADED: 'loaded',
1239
+ ERROR: 'error'
1240
+ };
1241
+ /**
1242
+ * @typedef {Object} ValuesGetter
1243
+ * @property {Object[]} values - The values data
1244
+ * @property {(LOAD_STATES)} state - The values data's loading state, to use for conditional rendering
1245
+ */
1246
+
1247
+ /**
1248
+ * A hook to load values for single and multiselect components.
1249
+ *
1250
+ * @param {Object} field - The form field to handle values for
1251
+ * @return {ValuesGetter} valuesGetter - A values getter object providing loading state and values
1252
+ */
1253
+
1254
+ function useValuesAsync (field) {
1255
+ const {
1256
+ valuesKey,
1257
+ values: staticValues
1258
+ } = field;
1259
+ const [valuesGetter, setValuesGetter] = hooks.useState({
1260
+ values: [],
1261
+ error: undefined,
1262
+ state: LOAD_STATES.LOADING
1263
+ });
1264
+
1265
+ const initialData = useService('form')._getState().initialData;
1266
+
1267
+ hooks.useEffect(() => {
1268
+ let values = [];
1269
+
1270
+ if (valuesKey !== undefined) {
1271
+ const keyedValues = (initialData || {})[valuesKey];
1272
+
1273
+ if (keyedValues && Array.isArray(keyedValues)) {
1274
+ values = keyedValues;
1275
+ }
1276
+ } else if (staticValues !== undefined) {
1277
+ values = Array.isArray(staticValues) ? staticValues : [];
1278
+ } else {
1279
+ setValuesGetter(getErrorState('No values source defined in the form definition'));
1280
+ return;
1281
+ }
1282
+
1283
+ setValuesGetter(buildLoadedState(values));
1284
+ }, [valuesKey, staticValues, initialData]);
1285
+ return valuesGetter;
1286
+ }
1287
+
1288
+ const getErrorState = error => ({
1289
+ values: [],
1290
+ error,
1291
+ state: LOAD_STATES.ERROR
1292
+ });
1293
+
1294
+ const buildLoadedState = values => ({
1295
+ values,
1296
+ error: undefined,
1297
+ state: LOAD_STATES.LOADED
1298
+ });
1299
+
1300
+ const type$6 = 'checklist';
1301
+ function Checklist(props) {
1302
+ const {
1303
+ disabled,
1304
+ errors = [],
1305
+ field,
1306
+ value = []
1307
+ } = props;
1308
+ const {
1309
+ description,
1310
+ id,
1311
+ label
1312
+ } = field;
1313
+
1314
+ const toggleCheckbox = v => {
1315
+ let newValue = [...value];
1316
+
1317
+ if (!newValue.includes(v)) {
1318
+ newValue.push(v);
1319
+ } else {
1320
+ newValue = newValue.filter(x => x != v);
1321
+ }
1322
+
1323
+ props.onChange({
1324
+ field,
1325
+ value: newValue
1326
+ });
1327
+ };
1328
+
1329
+ const {
1330
+ state: loadState,
1331
+ values: options
1332
+ } = useValuesAsync(field);
1333
+ const {
1334
+ formId
1335
+ } = hooks.useContext(FormContext);
1336
+ return jsxRuntime.jsxs("div", {
1337
+ class: formFieldClasses(type$6, errors),
1338
+ children: [jsxRuntime.jsx(Label, {
1339
+ label: label
1340
+ }), loadState == LOAD_STATES.LOADED && options.map((v, index) => {
1341
+ return jsxRuntime.jsx(Label, {
1342
+ id: prefixId(`${id}-${index}`, formId),
1343
+ label: v.label,
1344
+ required: false,
1345
+ children: jsxRuntime.jsx("input", {
1346
+ checked: value.includes(v.value),
1347
+ class: "fjs-input",
1348
+ disabled: disabled,
1349
+ id: prefixId(`${id}-${index}`, formId),
1350
+ type: "checkbox",
1351
+ onClick: () => toggleCheckbox(v.value)
1352
+ })
1353
+ }, `${id}-${index}`);
1354
+ }), jsxRuntime.jsx(Description, {
1355
+ description: description
1356
+ }), jsxRuntime.jsx(Errors, {
1357
+ errors: errors
1358
+ })]
1359
+ });
1360
+ }
1361
+
1362
+ Checklist.create = function (options = {}) {
1363
+ if (options.valuesKey) return options;
1364
+ return {
1365
+ values: [{
1366
+ label: 'Value',
1367
+ value: 'value'
1368
+ }],
1369
+ ...options
1370
+ };
1371
+ };
1372
+
1373
+ Checklist.type = type$6;
1374
+ Checklist.label = 'Checklist';
1375
+ Checklist.keyed = true;
1376
+ Checklist.emptyValue = [];
1377
+ Checklist.sanitizeValue = sanitizeMultiSelectValue;
1378
+
1167
1379
  const noop$1 = () => false;
1168
1380
 
1169
1381
  function FormField(props) {
@@ -1239,10 +1451,10 @@ Default.create = function (options = {}) {
1239
1451
  Default.type = 'default';
1240
1452
  Default.keyed = false;
1241
1453
 
1242
- /**
1243
- * This file must not be changed or exchanged.
1244
- *
1245
- * @see http://bpmn.io/license for more information.
1454
+ /**
1455
+ * This file must not be changed or exchanged.
1456
+ *
1457
+ * @see http://bpmn.io/license for more information.
1246
1458
  */
1247
1459
 
1248
1460
  function Logo() {
@@ -1327,7 +1539,7 @@ function PoweredBy(props) {
1327
1539
  }
1328
1540
 
1329
1541
  return jsxRuntime.jsxs(preact.Fragment, {
1330
- children: [compat.createPortal(jsxRuntime.jsx(Lightbox, {
1542
+ children: [React.createPortal(jsxRuntime.jsx(Lightbox, {
1331
1543
  open: open,
1332
1544
  onBackdropClick: toggleOpen(false)
1333
1545
  }), document.body), jsxRuntime.jsx(Link, {
@@ -1372,7 +1584,7 @@ function FormComponent(props) {
1372
1584
  });
1373
1585
  }
1374
1586
 
1375
- const type$4 = 'number';
1587
+ const type$5 = 'number';
1376
1588
  function Number(props) {
1377
1589
  const {
1378
1590
  disabled,
@@ -1393,10 +1605,11 @@ function Number(props) {
1393
1605
  const onChange = ({
1394
1606
  target
1395
1607
  }) => {
1396
- const parsedValue = parseInt(target.value, 10);
1397
1608
  props.onChange({
1398
1609
  field,
1399
- value: isNaN(parsedValue) ? null : parsedValue
1610
+ value: Number.sanitizeValue({
1611
+ value: target.value
1612
+ })
1400
1613
  });
1401
1614
  };
1402
1615
 
@@ -1404,7 +1617,7 @@ function Number(props) {
1404
1617
  formId
1405
1618
  } = hooks.useContext(FormContext);
1406
1619
  return jsxRuntime.jsxs("div", {
1407
- class: formFieldClasses(type$4, errors),
1620
+ class: formFieldClasses(type$5, errors),
1408
1621
  children: [jsxRuntime.jsx(Label, {
1409
1622
  id: prefixId(id, formId),
1410
1623
  label: label,
@@ -1429,12 +1642,19 @@ Number.create = function (options = {}) {
1429
1642
  };
1430
1643
  };
1431
1644
 
1432
- Number.type = type$4;
1645
+ Number.sanitizeValue = ({
1646
+ value
1647
+ }) => {
1648
+ const parsedValue = parseInt(value, 10);
1649
+ return isNaN(parsedValue) ? null : parsedValue;
1650
+ };
1651
+
1652
+ Number.type = type$5;
1433
1653
  Number.keyed = true;
1434
1654
  Number.label = 'Number';
1435
1655
  Number.emptyValue = null;
1436
1656
 
1437
- const type$3 = 'radio';
1657
+ const type$4 = 'radio';
1438
1658
  function Radio(props) {
1439
1659
  const {
1440
1660
  disabled,
@@ -1446,8 +1666,7 @@ function Radio(props) {
1446
1666
  description,
1447
1667
  id,
1448
1668
  label,
1449
- validate = {},
1450
- values
1669
+ validate = {}
1451
1670
  } = field;
1452
1671
  const {
1453
1672
  required
@@ -1460,26 +1679,30 @@ function Radio(props) {
1460
1679
  });
1461
1680
  };
1462
1681
 
1682
+ const {
1683
+ state: loadState,
1684
+ values: options
1685
+ } = useValuesAsync(field);
1463
1686
  const {
1464
1687
  formId
1465
1688
  } = hooks.useContext(FormContext);
1466
1689
  return jsxRuntime.jsxs("div", {
1467
- class: formFieldClasses(type$3, errors),
1690
+ class: formFieldClasses(type$4, errors),
1468
1691
  children: [jsxRuntime.jsx(Label, {
1469
1692
  label: label,
1470
1693
  required: required
1471
- }), values.map((v, index) => {
1694
+ }), loadState == LOAD_STATES.LOADED && options.map((option, index) => {
1472
1695
  return jsxRuntime.jsx(Label, {
1473
1696
  id: prefixId(`${id}-${index}`, formId),
1474
- label: v.label,
1697
+ label: option.label,
1475
1698
  required: false,
1476
1699
  children: jsxRuntime.jsx("input", {
1477
- checked: v.value === value,
1700
+ checked: option.value === value,
1478
1701
  class: "fjs-input",
1479
1702
  disabled: disabled,
1480
1703
  id: prefixId(`${id}-${index}`, formId),
1481
1704
  type: "radio",
1482
- onClick: () => onChange(v.value)
1705
+ onClick: () => onChange(option.value)
1483
1706
  })
1484
1707
  }, `${id}-${index}`);
1485
1708
  }), jsxRuntime.jsx(Description, {
@@ -1491,6 +1714,7 @@ function Radio(props) {
1491
1714
  }
1492
1715
 
1493
1716
  Radio.create = function (options = {}) {
1717
+ if (options.valuesKey) return options;
1494
1718
  return {
1495
1719
  values: [{
1496
1720
  label: 'Value',
@@ -1500,12 +1724,13 @@ Radio.create = function (options = {}) {
1500
1724
  };
1501
1725
  };
1502
1726
 
1503
- Radio.type = type$3;
1727
+ Radio.type = type$4;
1504
1728
  Radio.label = 'Radio';
1505
1729
  Radio.keyed = true;
1506
1730
  Radio.emptyValue = null;
1731
+ Radio.sanitizeValue = sanitizeSingleSelectValue;
1507
1732
 
1508
- const type$2 = 'select';
1733
+ const type$3 = 'select';
1509
1734
  function Select(props) {
1510
1735
  const {
1511
1736
  disabled,
@@ -1517,8 +1742,7 @@ function Select(props) {
1517
1742
  description,
1518
1743
  id,
1519
1744
  label,
1520
- validate = {},
1521
- values
1745
+ validate = {}
1522
1746
  } = field;
1523
1747
  const {
1524
1748
  required
@@ -1533,11 +1757,15 @@ function Select(props) {
1533
1757
  });
1534
1758
  };
1535
1759
 
1760
+ const {
1761
+ state: loadState,
1762
+ values: options
1763
+ } = useValuesAsync(field);
1536
1764
  const {
1537
1765
  formId
1538
1766
  } = hooks.useContext(FormContext);
1539
1767
  return jsxRuntime.jsxs("div", {
1540
- class: formFieldClasses(type$2, errors),
1768
+ class: formFieldClasses(type$3, errors),
1541
1769
  children: [jsxRuntime.jsx(Label, {
1542
1770
  id: prefixId(id, formId),
1543
1771
  label: label,
@@ -1550,10 +1778,10 @@ function Select(props) {
1550
1778
  value: value || '',
1551
1779
  children: [jsxRuntime.jsx("option", {
1552
1780
  value: ""
1553
- }), values.map((v, index) => {
1781
+ }), loadState == LOAD_STATES.LOADED && options.map((option, index) => {
1554
1782
  return jsxRuntime.jsx("option", {
1555
- value: v.value,
1556
- children: v.label
1783
+ value: option.value,
1784
+ children: option.label
1557
1785
  }, `${id}-${index}`);
1558
1786
  })]
1559
1787
  }), jsxRuntime.jsx(Description, {
@@ -1565,6 +1793,7 @@ function Select(props) {
1565
1793
  }
1566
1794
 
1567
1795
  Select.create = function (options = {}) {
1796
+ if (options.valuesKey) return options;
1568
1797
  return {
1569
1798
  values: [{
1570
1799
  label: 'Value',
@@ -1574,10 +1803,317 @@ Select.create = function (options = {}) {
1574
1803
  };
1575
1804
  };
1576
1805
 
1577
- Select.type = type$2;
1806
+ Select.type = type$3;
1578
1807
  Select.label = 'Select';
1579
1808
  Select.keyed = true;
1580
1809
  Select.emptyValue = null;
1810
+ Select.sanitizeValue = sanitizeSingleSelectValue;
1811
+
1812
+ function _extends() { _extends = 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.apply(this, arguments); }
1813
+ var CloseIcon = (({
1814
+ styles = {},
1815
+ ...props
1816
+ }) => /*#__PURE__*/React__default['default'].createElement("svg", _extends({
1817
+ width: "16",
1818
+ height: "16",
1819
+ fill: "none",
1820
+ xmlns: "http://www.w3.org/2000/svg"
1821
+ }, props), /*#__PURE__*/React__default['default'].createElement("path", {
1822
+ fillRule: "evenodd",
1823
+ clipRule: "evenodd",
1824
+ d: "M12 4.7l-.7-.7L8 7.3 4.7 4l-.7.7L7.3 8 4 11.3l.7.7L8 8.7l3.3 3.3.7-.7L8.7 8 12 4.7z",
1825
+ fill: "#000"
1826
+ })));
1827
+
1828
+ function useKeyDownAction(targetKey, action, listenerElement = window) {
1829
+ function downHandler({
1830
+ key
1831
+ }) {
1832
+ if (key === targetKey) {
1833
+ action();
1834
+ }
1835
+ }
1836
+
1837
+ hooks.useEffect(() => {
1838
+ listenerElement.addEventListener('keydown', downHandler);
1839
+ return () => {
1840
+ listenerElement.removeEventListener('keydown', downHandler);
1841
+ };
1842
+ });
1843
+ }
1844
+
1845
+ const DEFAULT_LABEL_GETTER = value => value;
1846
+
1847
+ const NOOP = () => {};
1848
+
1849
+ function DropdownList(props) {
1850
+ const {
1851
+ keyEventsListener = window,
1852
+ values = [],
1853
+ getLabel = DEFAULT_LABEL_GETTER,
1854
+ onValueSelected = NOOP,
1855
+ height = 235,
1856
+ emptyListMessage = 'No results'
1857
+ } = props;
1858
+ const [mouseControl, setMouseControl] = hooks.useState(true);
1859
+ const [focusedValueIndex, setFocusedValueIndex] = hooks.useState(0);
1860
+ const dropdownContainer = hooks.useRef();
1861
+ const mouseScreenPos = hooks.useRef();
1862
+ const focusedItem = hooks.useMemo(() => values.length ? values[focusedValueIndex] : null, [focusedValueIndex, values]);
1863
+ const changeFocusedValueIndex = hooks.useCallback(delta => {
1864
+ setFocusedValueIndex(x => Math.min(Math.max(0, x + delta), values.length - 1));
1865
+ }, [values.length]);
1866
+ hooks.useEffect(() => {
1867
+ if (focusedValueIndex === 0) return;
1868
+
1869
+ if (!focusedValueIndex || !values.length) {
1870
+ setFocusedValueIndex(0);
1871
+ } else if (focusedValueIndex >= values.length) {
1872
+ setFocusedValueIndex(values.length - 1);
1873
+ }
1874
+ }, [focusedValueIndex, values.length]);
1875
+ useKeyDownAction('ArrowUp', () => {
1876
+ if (values.length) {
1877
+ changeFocusedValueIndex(-1);
1878
+ setMouseControl(false);
1879
+ }
1880
+ }, keyEventsListener);
1881
+ useKeyDownAction('ArrowDown', () => {
1882
+ if (values.length) {
1883
+ changeFocusedValueIndex(1);
1884
+ setMouseControl(false);
1885
+ }
1886
+ }, keyEventsListener);
1887
+ useKeyDownAction('Enter', () => {
1888
+ if (focusedItem) {
1889
+ onValueSelected(focusedItem);
1890
+ }
1891
+ }, keyEventsListener);
1892
+ hooks.useEffect(() => {
1893
+ const individualEntries = dropdownContainer.current.children;
1894
+
1895
+ if (individualEntries.length && !mouseControl) {
1896
+ individualEntries[focusedValueIndex].scrollIntoView({
1897
+ block: 'nearest',
1898
+ inline: 'nearest'
1899
+ });
1900
+ }
1901
+ }, [focusedValueIndex, mouseControl]);
1902
+
1903
+ const mouseMove = (e, i) => {
1904
+ const userMoved = !mouseScreenPos.current || mouseScreenPos.current.x !== e.screenX && mouseScreenPos.current.y !== e.screenY;
1905
+
1906
+ if (userMoved) {
1907
+ mouseScreenPos.current = {
1908
+ x: e.screenX,
1909
+ y: e.screenY
1910
+ };
1911
+
1912
+ if (!mouseControl) {
1913
+ setMouseControl(true);
1914
+ setFocusedValueIndex(i);
1915
+ }
1916
+ }
1917
+ };
1918
+
1919
+ return jsxRuntime.jsxs("div", {
1920
+ ref: dropdownContainer,
1921
+ tabIndex: -1,
1922
+ class: "fjs-dropdownlist",
1923
+ style: {
1924
+ maxHeight: height
1925
+ },
1926
+ children: [!!values.length && values.map((v, i) => {
1927
+ return jsxRuntime.jsx("div", {
1928
+ class: 'fjs-dropdownlist-item' + (focusedValueIndex === i ? ' focused' : ''),
1929
+ onMouseMove: e => mouseMove(e, i),
1930
+ onMouseEnter: mouseControl ? () => setFocusedValueIndex(i) : undefined,
1931
+ onMouseDown: e => {
1932
+ e.preventDefault();
1933
+ onValueSelected(v);
1934
+ },
1935
+ children: getLabel(v)
1936
+ });
1937
+ }), !values.length && jsxRuntime.jsx("div", {
1938
+ class: "fjs-dropdownlist-empty",
1939
+ children: emptyListMessage
1940
+ })]
1941
+ });
1942
+ }
1943
+
1944
+ const type$2 = 'taglist';
1945
+ function Taglist(props) {
1946
+ const {
1947
+ disabled,
1948
+ errors = [],
1949
+ field,
1950
+ value: values = []
1951
+ } = props;
1952
+ const {
1953
+ description,
1954
+ id,
1955
+ label
1956
+ } = field;
1957
+ const {
1958
+ formId
1959
+ } = hooks.useContext(FormContext);
1960
+ const [filter, setFilter] = hooks.useState('');
1961
+ const [filteredOptions, setFilteredOptions] = hooks.useState([]);
1962
+ const [isDropdownExpanded, setIsDropdownExpanded] = hooks.useState(false);
1963
+ const [hasOptionsLeft, setHasOptionsLeft] = hooks.useState(true);
1964
+ const [isEscapeClosed, setIsEscapeClose] = hooks.useState(false);
1965
+ const searchbarRef = hooks.useRef();
1966
+ const {
1967
+ state: loadState,
1968
+ values: options
1969
+ } = useValuesAsync(field); // We cache a map of option values to their index so that we don't need to search the whole options array every time to correlate the label
1970
+
1971
+ const valueToOptionMap = hooks.useMemo(() => Object.assign({}, ...options.map((o, x) => ({
1972
+ [o.value]: options[x]
1973
+ }))), [options]); // Usage of stringify is necessary here because we want this effect to only trigger when there is a value change to the array
1974
+
1975
+ hooks.useEffect(() => {
1976
+ if (loadState === LOAD_STATES.LOADED) {
1977
+ setFilteredOptions(options.filter(o => o.label && o.value && o.label.toLowerCase().includes(filter.toLowerCase()) && !values.includes(o.value)));
1978
+ } else {
1979
+ setFilteredOptions([]);
1980
+ }
1981
+ }, [filter, JSON.stringify(values), options, loadState]);
1982
+ hooks.useEffect(() => {
1983
+ setHasOptionsLeft(options.length > values.length);
1984
+ }, [options.length, values.length]);
1985
+
1986
+ const onFilterChange = ({
1987
+ target
1988
+ }) => {
1989
+ setIsEscapeClose(false);
1990
+ setFilter(target.value);
1991
+ };
1992
+
1993
+ const selectValue = value => {
1994
+ if (filter) {
1995
+ setFilter('');
1996
+ } // Ensure values cannot be double selected due to latency
1997
+
1998
+
1999
+ if (values.at(-1) === value) {
2000
+ return;
2001
+ }
2002
+
2003
+ props.onChange({
2004
+ value: [...values, value],
2005
+ field
2006
+ });
2007
+ };
2008
+
2009
+ const deselectValue = value => {
2010
+ props.onChange({
2011
+ value: values.filter(v => v != value),
2012
+ field
2013
+ });
2014
+ };
2015
+
2016
+ const onInputKeyDown = e => {
2017
+ switch (e.key) {
2018
+ case 'ArrowUp':
2019
+ case 'ArrowDown':
2020
+ // We do not want the cursor to seek in the search field when we press up and down
2021
+ e.preventDefault();
2022
+ break;
2023
+
2024
+ case 'Backspace':
2025
+ if (!filter && values.length) {
2026
+ deselectValue(values[values.length - 1]);
2027
+ }
2028
+
2029
+ break;
2030
+
2031
+ case 'Escape':
2032
+ setIsEscapeClose(true);
2033
+ break;
2034
+
2035
+ case 'Enter':
2036
+ if (isEscapeClosed) {
2037
+ setIsEscapeClose(false);
2038
+ }
2039
+
2040
+ break;
2041
+ }
2042
+ };
2043
+
2044
+ return jsxRuntime.jsxs("div", {
2045
+ class: formFieldClasses(type$2, errors),
2046
+ children: [jsxRuntime.jsx(Label, {
2047
+ label: label,
2048
+ id: prefixId(id, formId)
2049
+ }), jsxRuntime.jsxs("div", {
2050
+ class: classNames__default['default']('fjs-taglist', {
2051
+ 'disabled': disabled
2052
+ }),
2053
+ children: [!disabled && loadState === LOAD_STATES.LOADED && values.map(v => {
2054
+ return jsxRuntime.jsxs("div", {
2055
+ class: "fjs-taglist-tag",
2056
+ onMouseDown: e => e.preventDefault(),
2057
+ children: [jsxRuntime.jsx("span", {
2058
+ class: "fjs-taglist-tag-label",
2059
+ children: valueToOptionMap[v] ? valueToOptionMap[v].label : `unexpected value{${v}}`
2060
+ }), jsxRuntime.jsx("span", {
2061
+ class: "fjs-taglist-tag-remove",
2062
+ onMouseDown: () => deselectValue(v),
2063
+ children: jsxRuntime.jsx(CloseIcon, {})
2064
+ })]
2065
+ });
2066
+ }), jsxRuntime.jsx("input", {
2067
+ disabled: disabled,
2068
+ class: "fjs-taglist-input",
2069
+ ref: searchbarRef,
2070
+ id: prefixId(`${id}-search`, formId),
2071
+ onChange: onFilterChange,
2072
+ type: "text",
2073
+ value: filter,
2074
+ placeholder: 'Search',
2075
+ autoComplete: "off",
2076
+ onKeyDown: e => onInputKeyDown(e),
2077
+ onMouseDown: () => setIsEscapeClose(false),
2078
+ onFocus: () => setIsDropdownExpanded(true),
2079
+ onBlur: () => {
2080
+ setIsDropdownExpanded(false);
2081
+ setFilter('');
2082
+ }
2083
+ })]
2084
+ }), jsxRuntime.jsx("div", {
2085
+ class: "fjs-taglist-anchor",
2086
+ children: !disabled && loadState === LOAD_STATES.LOADED && isDropdownExpanded && !isEscapeClosed && jsxRuntime.jsx(DropdownList, {
2087
+ values: filteredOptions,
2088
+ getLabel: o => o.label,
2089
+ onValueSelected: o => selectValue(o.value),
2090
+ emptyListMessage: hasOptionsLeft ? 'No results' : 'All values selected',
2091
+ listenerElement: searchbarRef.current
2092
+ })
2093
+ }), jsxRuntime.jsx(Description, {
2094
+ description: description
2095
+ }), jsxRuntime.jsx(Errors, {
2096
+ errors: errors
2097
+ })]
2098
+ });
2099
+ }
2100
+
2101
+ Taglist.create = function (options = {}) {
2102
+ if (options.valuesKey) return options;
2103
+ return {
2104
+ values: [{
2105
+ label: 'Value',
2106
+ value: 'value'
2107
+ }],
2108
+ ...options
2109
+ };
2110
+ };
2111
+
2112
+ Taglist.type = type$2;
2113
+ Taglist.label = 'Taglist';
2114
+ Taglist.keyed = true;
2115
+ Taglist.emptyValue = [];
2116
+ Taglist.sanitizeValue = sanitizeMultiSelectValue;
1581
2117
 
1582
2118
  const type$1 = 'text';
1583
2119
  function Text(props) {
@@ -1667,7 +2203,11 @@ Textfield.label = 'Text Field';
1667
2203
  Textfield.keyed = true;
1668
2204
  Textfield.emptyValue = '';
1669
2205
 
1670
- const formFields = [Button, Checkbox, Default, Number, Radio, Select, Text, Textfield];
2206
+ Textfield.sanitizeValue = ({
2207
+ value
2208
+ }) => minDash.isArray(value) || minDash.isObject(value) ? null : String(value);
2209
+
2210
+ const formFields = [Button, Checkbox, Checklist, Default, Number, Radio, Select, Taglist, Text, Textfield];
1671
2211
 
1672
2212
  class FormFields {
1673
2213
  constructor() {
@@ -2154,7 +2694,7 @@ class Form {
2154
2694
 
2155
2695
  }
2156
2696
 
2157
- const schemaVersion = 4;
2697
+ const schemaVersion = 5;
2158
2698
  /**
2159
2699
  * @typedef { import('./types').CreateFormOptions } CreateFormOptions
2160
2700
  */
@@ -2181,6 +2721,7 @@ function createForm(options) {
2181
2721
 
2182
2722
  exports.Button = Button;
2183
2723
  exports.Checkbox = Checkbox;
2724
+ exports.Checklist = Checklist;
2184
2725
  exports.Default = Default;
2185
2726
  exports.Form = Form;
2186
2727
  exports.FormComponent = FormComponent;
@@ -2191,6 +2732,7 @@ exports.FormRenderContext = FormRenderContext;
2191
2732
  exports.Number = Number;
2192
2733
  exports.Radio = Radio;
2193
2734
  exports.Select = Select;
2735
+ exports.Taglist = Taglist;
2194
2736
  exports.Text = Text;
2195
2737
  exports.Textfield = Textfield;
2196
2738
  exports.clone = clone;
@@ -2201,6 +2743,7 @@ exports.findErrors = findErrors;
2201
2743
  exports.formFields = formFields;
2202
2744
  exports.generateIdForType = generateIdForType;
2203
2745
  exports.generateIndexForType = generateIndexForType;
2746
+ exports.getSchemaVariables = getSchemaVariables;
2204
2747
  exports.isRequired = isRequired;
2205
2748
  exports.pathParse = pathParse;
2206
2749
  exports.pathStringify = pathStringify;