@bpmn-io/form-js-viewer 0.7.1 → 0.8.0-alpha.1

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
@@ -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,45 +601,8 @@ 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
 
@@ -699,6 +665,41 @@ function generateIdForType(type) {
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
  /**
@@ -723,7 +724,7 @@ class Importer {
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 {
@@ -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
 
@@ -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
 
@@ -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) {
@@ -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,314 @@ 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 || 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 [selectedValues, setSelectedValues] = hooks.useState([]);
1962
+ const [filteredValues, setFilteredValues] = hooks.useState([]);
1963
+ const [isDropdownExpanded, setIsDropdownExpanded] = hooks.useState(false);
1964
+ const [hasValuesLeft, setHasValuesLeft] = hooks.useState(true);
1965
+ const [isEscapeClosed, setIsEscapeClose] = hooks.useState(false);
1966
+ const searchbarRef = hooks.useRef();
1967
+ const {
1968
+ state: loadState,
1969
+ values: options
1970
+ } = useValuesAsync(field); // Usage of stringify is necessary here because we want this effect to only trigger when there is a value change to the array
1971
+
1972
+ hooks.useEffect(() => {
1973
+ if (loadState === LOAD_STATES.LOADED) {
1974
+ const selectedValues = values.map(v => options.find(o => o.value === v)).filter(v => v !== undefined);
1975
+ setSelectedValues(selectedValues);
1976
+ } else {
1977
+ setSelectedValues([]);
1978
+ }
1979
+ }, [JSON.stringify(values), options, loadState]);
1980
+ hooks.useEffect(() => {
1981
+ if (loadState === LOAD_STATES.LOADED) {
1982
+ setFilteredValues(options.filter(o => o.label && o.value && o.label.toLowerCase().includes(filter.toLowerCase()) && !values.includes(o.value)));
1983
+ } else {
1984
+ setFilteredValues([]);
1985
+ }
1986
+ }, [filter, JSON.stringify(values), options]);
1987
+ hooks.useEffect(() => {
1988
+ setHasValuesLeft(selectedValues.length < options.length);
1989
+ }, [selectedValues.length, options.length]);
1990
+
1991
+ const onFilterChange = ({
1992
+ target
1993
+ }) => {
1994
+ setIsEscapeClose(false);
1995
+ setFilter(target.value);
1996
+ };
1997
+
1998
+ const selectValue = option => {
1999
+ setFilter('');
2000
+ props.onChange({
2001
+ value: [...values, option.value],
2002
+ field
2003
+ });
2004
+ };
2005
+
2006
+ const deselectValue = option => {
2007
+ props.onChange({
2008
+ value: values.filter(v => v != option.value),
2009
+ field
2010
+ });
2011
+ };
2012
+
2013
+ const onInputKeyDown = e => {
2014
+ switch (e.key) {
2015
+ case 'ArrowUp':
2016
+ case 'ArrowDown':
2017
+ // We do not want the cursor to seek in the search field when we press up and down
2018
+ e.preventDefault();
2019
+ break;
2020
+
2021
+ case 'Backspace':
2022
+ if (!filter && selectedValues.length) {
2023
+ deselectValue(selectedValues[selectedValues.length - 1]);
2024
+ }
2025
+
2026
+ break;
2027
+
2028
+ case 'Escape':
2029
+ setIsEscapeClose(true);
2030
+ break;
2031
+
2032
+ case 'Enter':
2033
+ if (isEscapeClosed) {
2034
+ setIsEscapeClose(false);
2035
+ }
2036
+
2037
+ break;
2038
+ }
2039
+ };
2040
+
2041
+ return jsxRuntime.jsxs("div", {
2042
+ class: formFieldClasses(type$2, errors),
2043
+ children: [jsxRuntime.jsx(Label, {
2044
+ label: label,
2045
+ id: prefixId(id, formId)
2046
+ }), jsxRuntime.jsxs("div", {
2047
+ class: classNames__default['default']('fjs-taglist', {
2048
+ 'disabled': disabled
2049
+ }),
2050
+ children: [!disabled && loadState === LOAD_STATES.LOADED && selectedValues.map(sv => {
2051
+ return jsxRuntime.jsxs("div", {
2052
+ class: "fjs-taglist-tag",
2053
+ onMouseDown: e => e.preventDefault(),
2054
+ children: [jsxRuntime.jsx("span", {
2055
+ class: "fjs-taglist-tag-label",
2056
+ children: sv.label
2057
+ }), jsxRuntime.jsx("span", {
2058
+ class: "fjs-taglist-tag-remove",
2059
+ onMouseDown: () => deselectValue(sv),
2060
+ children: jsxRuntime.jsx(CloseIcon, {})
2061
+ })]
2062
+ });
2063
+ }), jsxRuntime.jsx("input", {
2064
+ disabled: disabled,
2065
+ class: "fjs-taglist-input",
2066
+ ref: searchbarRef,
2067
+ id: prefixId(`${id}-search`, formId),
2068
+ onChange: onFilterChange,
2069
+ type: "text",
2070
+ value: filter,
2071
+ placeholder: 'Search',
2072
+ autoComplete: "off",
2073
+ onKeyDown: e => onInputKeyDown(e),
2074
+ onMouseDown: () => setIsEscapeClose(false),
2075
+ onFocus: () => setIsDropdownExpanded(true),
2076
+ onBlur: () => {
2077
+ setIsDropdownExpanded(false);
2078
+ setFilter('');
2079
+ }
2080
+ })]
2081
+ }), jsxRuntime.jsx("div", {
2082
+ class: "fjs-taglist-anchor",
2083
+ children: !disabled && loadState === LOAD_STATES.LOADED && isDropdownExpanded && !isEscapeClosed && jsxRuntime.jsx(DropdownList, {
2084
+ values: filteredValues,
2085
+ getLabel: v => v.label,
2086
+ onValueSelected: v => selectValue(v),
2087
+ emptyListMessage: hasValuesLeft ? 'No results' : 'All values selected',
2088
+ listenerElement: searchbarRef.current
2089
+ })
2090
+ }), jsxRuntime.jsx(Description, {
2091
+ description: description
2092
+ }), jsxRuntime.jsx(Errors, {
2093
+ errors: errors
2094
+ })]
2095
+ });
2096
+ }
2097
+
2098
+ Taglist.create = function (options = {}) {
2099
+ if (options.valuesKey) return options;
2100
+ return {
2101
+ values: [{
2102
+ label: 'Value',
2103
+ value: 'value'
2104
+ }],
2105
+ ...options
2106
+ };
2107
+ };
2108
+
2109
+ Taglist.type = type$2;
2110
+ Taglist.label = 'Taglist';
2111
+ Taglist.keyed = true;
2112
+ Taglist.emptyValue = [];
2113
+ Taglist.sanitizeValue = sanitizeMultiSelectValue;
1581
2114
 
1582
2115
  const type$1 = 'text';
1583
2116
  function Text(props) {
@@ -1667,7 +2200,11 @@ Textfield.label = 'Text Field';
1667
2200
  Textfield.keyed = true;
1668
2201
  Textfield.emptyValue = '';
1669
2202
 
1670
- const formFields = [Button, Checkbox, Default, Number, Radio, Select, Text, Textfield];
2203
+ Textfield.sanitizeValue = ({
2204
+ value
2205
+ }) => minDash.isArray(value) || minDash.isObject(value) ? null : String(value);
2206
+
2207
+ const formFields = [Button, Checkbox, Checklist, Default, Number, Radio, Select, Taglist, Text, Textfield];
1671
2208
 
1672
2209
  class FormFields {
1673
2210
  constructor() {
@@ -2154,7 +2691,7 @@ class Form {
2154
2691
 
2155
2692
  }
2156
2693
 
2157
- const schemaVersion = 4;
2694
+ const schemaVersion = 5;
2158
2695
  /**
2159
2696
  * @typedef { import('./types').CreateFormOptions } CreateFormOptions
2160
2697
  */
@@ -2181,6 +2718,7 @@ function createForm(options) {
2181
2718
 
2182
2719
  exports.Button = Button;
2183
2720
  exports.Checkbox = Checkbox;
2721
+ exports.Checklist = Checklist;
2184
2722
  exports.Default = Default;
2185
2723
  exports.Form = Form;
2186
2724
  exports.FormComponent = FormComponent;
@@ -2191,6 +2729,7 @@ exports.FormRenderContext = FormRenderContext;
2191
2729
  exports.Number = Number;
2192
2730
  exports.Radio = Radio;
2193
2731
  exports.Select = Select;
2732
+ exports.Taglist = Taglist;
2194
2733
  exports.Text = Text;
2195
2734
  exports.Textfield = Textfield;
2196
2735
  exports.clone = clone;
@@ -2201,6 +2740,7 @@ exports.findErrors = findErrors;
2201
2740
  exports.formFields = formFields;
2202
2741
  exports.generateIdForType = generateIdForType;
2203
2742
  exports.generateIndexForType = generateIndexForType;
2743
+ exports.getSchemaVariables = getSchemaVariables;
2204
2744
  exports.isRequired = isRequired;
2205
2745
  exports.pathParse = pathParse;
2206
2746
  exports.pathStringify = pathStringify;