@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.es.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import Ids from 'ids';
2
- import { isArray, isFunction, isNumber, bind, assign, isNil, get, isUndefined, set, isString } from 'min-dash';
2
+ import { isArray, isFunction, isNumber, bind, assign, isNil, get, isUndefined, isObject, set, isString } from 'min-dash';
3
3
  import snarkdown from '@bpmn-io/snarkdown';
4
4
  import { jsx, jsxs } from 'preact/jsx-runtime';
5
- import { useContext, useState, useCallback } from 'preact/hooks';
5
+ import { useContext, useState, useEffect, useRef, useMemo, useCallback } from 'preact/hooks';
6
6
  import { createContext, createElement, Fragment, render } from 'preact';
7
+ import React, { createPortal } from 'preact/compat';
8
+ import classNames from 'classnames';
7
9
  import Markup from 'preact-markup';
8
- import { createPortal } from 'preact/compat';
9
10
  import { Injector } from 'didi';
10
11
 
11
12
  var FN_REF = '__fn';
@@ -588,45 +589,8 @@ class FormFieldRegistry {
588
589
  FormFieldRegistry.$inject = ['eventBus'];
589
590
 
590
591
  function createInjector(bootstrapModules) {
591
- const modules = [],
592
- components = [];
593
-
594
- function hasModule(module) {
595
- return modules.includes(module);
596
- }
597
-
598
- function addModule(module) {
599
- modules.push(module);
600
- }
601
-
602
- function visit(module) {
603
- if (hasModule(module)) {
604
- return;
605
- }
606
-
607
- (module.__depends__ || []).forEach(visit);
608
-
609
- if (hasModule(module)) {
610
- return;
611
- }
612
-
613
- addModule(module);
614
- (module.__init__ || []).forEach(function (component) {
615
- components.push(component);
616
- });
617
- }
618
-
619
- bootstrapModules.forEach(visit);
620
- const injector = new Injector(modules);
621
- components.forEach(function (component) {
622
- try {
623
- injector[typeof component === 'string' ? 'get' : 'invoke'](component);
624
- } catch (err) {
625
- console.error('Failed to instantiate component');
626
- console.error(err.stack);
627
- throw err;
628
- }
629
- });
592
+ const injector = new Injector(bootstrapModules);
593
+ injector.init();
630
594
  return injector;
631
595
  }
632
596
 
@@ -689,6 +653,41 @@ function generateIdForType(type) {
689
653
  function clone(data, replacer) {
690
654
  return JSON.parse(JSON.stringify(data, replacer));
691
655
  }
656
+ /**
657
+ * Parse the schema for input variables a form might make use of
658
+ *
659
+ * @param {any} schema
660
+ *
661
+ * @return {string[]}
662
+ */
663
+
664
+ function getSchemaVariables(schema) {
665
+ if (!schema.components) {
666
+ return [];
667
+ }
668
+
669
+ return schema.components.reduce((variables, component) => {
670
+ const {
671
+ key,
672
+ valuesKey,
673
+ type
674
+ } = component;
675
+
676
+ if (['text', 'button'].includes(type)) {
677
+ return variables;
678
+ }
679
+
680
+ if (key) {
681
+ variables = [...variables, key];
682
+ }
683
+
684
+ if (valuesKey && !variables.includes(valuesKey)) {
685
+ variables = [...variables, valuesKey];
686
+ }
687
+
688
+ return variables;
689
+ }, []);
690
+ }
692
691
 
693
692
  class Importer {
694
693
  /**
@@ -713,7 +712,7 @@ class Importer {
713
712
 
714
713
 
715
714
  importSchema(schema, data = {}) {
716
- // TODO: Add warnings
715
+ // TODO: Add warnings - https://github.com/bpmn-io/form-js/issues/289
717
716
  const warnings = [];
718
717
 
719
718
  try {
@@ -807,19 +806,39 @@ class Importer {
807
806
  const {
808
807
  defaultValue,
809
808
  _path,
810
- type
811
- } = formField;
812
-
813
- if (!_path) {
814
- return importedData;
815
- } // (1) try to get value from data
816
- // (2) try to get default value from form field
817
- // (3) get empty value from form field
818
-
809
+ type,
810
+ valuesKey
811
+ } = formField; // get values defined via valuesKey
812
+
813
+ if (valuesKey) {
814
+ importedData = { ...importedData,
815
+ [valuesKey]: get(data, [valuesKey])
816
+ };
817
+ } // try to get value from data
818
+ // if unavailable - try to get default value from form field
819
+ // if unavailable - get empty value from form field
820
+
821
+
822
+ if (_path) {
823
+ const fieldImplementation = this._formFields.get(type);
824
+
825
+ let valueData = get(data, _path);
826
+
827
+ if (!isUndefined(valueData) && fieldImplementation.sanitizeValue) {
828
+ valueData = fieldImplementation.sanitizeValue({
829
+ formField,
830
+ data,
831
+ value: valueData
832
+ });
833
+ }
834
+
835
+ const initialFieldValue = !isUndefined(valueData) ? valueData : !isUndefined(defaultValue) ? defaultValue : fieldImplementation.emptyValue;
836
+ importedData = { ...importedData,
837
+ [_path[0]]: initialFieldValue
838
+ };
839
+ }
819
840
 
820
- return { ...importedData,
821
- [_path[0]]: get(data, _path, isUndefined(defaultValue) ? this._formFields.get(type).emptyValue : defaultValue)
822
- };
841
+ return importedData;
823
842
  }, {});
824
843
  }
825
844
 
@@ -979,8 +998,48 @@ function safeMarkdown(markdown) {
979
998
  const html = markdownToHTML(markdown);
980
999
  return sanitizeHTML(html);
981
1000
  }
1001
+ function sanitizeSingleSelectValue(options) {
1002
+ const {
1003
+ formField,
1004
+ data,
1005
+ value
1006
+ } = options;
1007
+ const {
1008
+ valuesKey,
1009
+ values
1010
+ } = formField;
1011
+
1012
+ try {
1013
+ const validValues = (valuesKey ? get(data, [valuesKey]) : values).map(v => v.value) || [];
1014
+ return validValues.includes(value) ? value : null;
1015
+ } catch (error) {
1016
+ // use default value in case of formatting error
1017
+ // TODO(@Skaiir): log a warning when this happens - https://github.com/bpmn-io/form-js/issues/289
1018
+ return null;
1019
+ }
1020
+ }
1021
+ function sanitizeMultiSelectValue(options) {
1022
+ const {
1023
+ formField,
1024
+ data,
1025
+ value
1026
+ } = options;
1027
+ const {
1028
+ valuesKey,
1029
+ values
1030
+ } = formField;
1031
+
1032
+ try {
1033
+ const validValues = (valuesKey ? get(data, [valuesKey]) : values).map(v => v.value) || [];
1034
+ return value.filter(v => validValues.includes(v));
1035
+ } catch (error) {
1036
+ // use default value in case of formatting error
1037
+ // TODO(@Skaiir): log a warning when this happens - https://github.com/bpmn-io/form-js/issues/289
1038
+ return [];
1039
+ }
1040
+ }
982
1041
 
983
- const type$6 = 'button';
1042
+ const type$8 = 'button';
984
1043
  function Button(props) {
985
1044
  const {
986
1045
  disabled,
@@ -990,7 +1049,7 @@ function Button(props) {
990
1049
  action = 'submit'
991
1050
  } = field;
992
1051
  return jsx("div", {
993
- class: formFieldClasses(type$6),
1052
+ class: formFieldClasses(type$8),
994
1053
  children: jsx("button", {
995
1054
  class: "fjs-button",
996
1055
  type: action,
@@ -1007,7 +1066,7 @@ Button.create = function (options = {}) {
1007
1066
  };
1008
1067
  };
1009
1068
 
1010
- Button.type = type$6;
1069
+ Button.type = type$8;
1011
1070
  Button.label = 'Button';
1012
1071
  Button.keyed = true;
1013
1072
 
@@ -1089,7 +1148,7 @@ function Label(props) {
1089
1148
  });
1090
1149
  }
1091
1150
 
1092
- const type$5 = 'checkbox';
1151
+ const type$7 = 'checkbox';
1093
1152
  function Checkbox(props) {
1094
1153
  const {
1095
1154
  disabled,
@@ -1116,7 +1175,7 @@ function Checkbox(props) {
1116
1175
  formId
1117
1176
  } = useContext(FormContext);
1118
1177
  return jsxs("div", {
1119
- class: formFieldClasses(type$5, errors),
1178
+ class: formFieldClasses(type$7, errors),
1120
1179
  children: [jsx(Label, {
1121
1180
  id: prefixId(id, formId),
1122
1181
  label: label,
@@ -1142,11 +1201,15 @@ Checkbox.create = function (options = {}) {
1142
1201
  };
1143
1202
  };
1144
1203
 
1145
- Checkbox.type = type$5;
1204
+ Checkbox.type = type$7;
1146
1205
  Checkbox.label = 'Checkbox';
1147
1206
  Checkbox.keyed = true;
1148
1207
  Checkbox.emptyValue = false;
1149
1208
 
1209
+ Checkbox.sanitizeValue = ({
1210
+ value
1211
+ }) => value === true;
1212
+
1150
1213
  function useService (type, strict) {
1151
1214
  const {
1152
1215
  getService
@@ -1154,6 +1217,153 @@ function useService (type, strict) {
1154
1217
  return getService(type, strict);
1155
1218
  }
1156
1219
 
1220
+ /**
1221
+ * @enum { String }
1222
+ */
1223
+
1224
+ const LOAD_STATES = {
1225
+ LOADING: 'loading',
1226
+ LOADED: 'loaded',
1227
+ ERROR: 'error'
1228
+ };
1229
+ /**
1230
+ * @typedef {Object} ValuesGetter
1231
+ * @property {Object[]} values - The values data
1232
+ * @property {(LOAD_STATES)} state - The values data's loading state, to use for conditional rendering
1233
+ */
1234
+
1235
+ /**
1236
+ * A hook to load values for single and multiselect components.
1237
+ *
1238
+ * @param {Object} field - The form field to handle values for
1239
+ * @return {ValuesGetter} valuesGetter - A values getter object providing loading state and values
1240
+ */
1241
+
1242
+ function useValuesAsync (field) {
1243
+ const {
1244
+ valuesKey,
1245
+ values: staticValues
1246
+ } = field;
1247
+ const [valuesGetter, setValuesGetter] = useState({
1248
+ values: [],
1249
+ error: undefined,
1250
+ state: LOAD_STATES.LOADING
1251
+ });
1252
+
1253
+ const initialData = useService('form')._getState().initialData;
1254
+
1255
+ useEffect(() => {
1256
+ let values = [];
1257
+
1258
+ if (valuesKey !== undefined) {
1259
+ const keyedValues = (initialData || {})[valuesKey];
1260
+
1261
+ if (keyedValues && Array.isArray(keyedValues)) {
1262
+ values = keyedValues;
1263
+ }
1264
+ } else if (staticValues !== undefined) {
1265
+ values = Array.isArray(staticValues) ? staticValues : [];
1266
+ } else {
1267
+ setValuesGetter(getErrorState('No values source defined in the form definition'));
1268
+ return;
1269
+ }
1270
+
1271
+ setValuesGetter(buildLoadedState(values));
1272
+ }, [valuesKey, staticValues, initialData]);
1273
+ return valuesGetter;
1274
+ }
1275
+
1276
+ const getErrorState = error => ({
1277
+ values: [],
1278
+ error,
1279
+ state: LOAD_STATES.ERROR
1280
+ });
1281
+
1282
+ const buildLoadedState = values => ({
1283
+ values,
1284
+ error: undefined,
1285
+ state: LOAD_STATES.LOADED
1286
+ });
1287
+
1288
+ const type$6 = 'checklist';
1289
+ function Checklist(props) {
1290
+ const {
1291
+ disabled,
1292
+ errors = [],
1293
+ field,
1294
+ value = []
1295
+ } = props;
1296
+ const {
1297
+ description,
1298
+ id,
1299
+ label
1300
+ } = field;
1301
+
1302
+ const toggleCheckbox = v => {
1303
+ let newValue = [...value];
1304
+
1305
+ if (!newValue.includes(v)) {
1306
+ newValue.push(v);
1307
+ } else {
1308
+ newValue = newValue.filter(x => x != v);
1309
+ }
1310
+
1311
+ props.onChange({
1312
+ field,
1313
+ value: newValue
1314
+ });
1315
+ };
1316
+
1317
+ const {
1318
+ state: loadState,
1319
+ values: options
1320
+ } = useValuesAsync(field);
1321
+ const {
1322
+ formId
1323
+ } = useContext(FormContext);
1324
+ return jsxs("div", {
1325
+ class: formFieldClasses(type$6, errors),
1326
+ children: [jsx(Label, {
1327
+ label: label
1328
+ }), loadState == LOAD_STATES.LOADED && options.map((v, index) => {
1329
+ return jsx(Label, {
1330
+ id: prefixId(`${id}-${index}`, formId),
1331
+ label: v.label,
1332
+ required: false,
1333
+ children: jsx("input", {
1334
+ checked: value.includes(v.value),
1335
+ class: "fjs-input",
1336
+ disabled: disabled,
1337
+ id: prefixId(`${id}-${index}`, formId),
1338
+ type: "checkbox",
1339
+ onClick: () => toggleCheckbox(v.value)
1340
+ })
1341
+ }, `${id}-${index}`);
1342
+ }), jsx(Description, {
1343
+ description: description
1344
+ }), jsx(Errors, {
1345
+ errors: errors
1346
+ })]
1347
+ });
1348
+ }
1349
+
1350
+ Checklist.create = function (options = {}) {
1351
+ if (options.valuesKey) return options;
1352
+ return {
1353
+ values: [{
1354
+ label: 'Value',
1355
+ value: 'value'
1356
+ }],
1357
+ ...options
1358
+ };
1359
+ };
1360
+
1361
+ Checklist.type = type$6;
1362
+ Checklist.label = 'Checklist';
1363
+ Checklist.keyed = true;
1364
+ Checklist.emptyValue = [];
1365
+ Checklist.sanitizeValue = sanitizeMultiSelectValue;
1366
+
1157
1367
  const noop$1 = () => false;
1158
1368
 
1159
1369
  function FormField(props) {
@@ -1362,7 +1572,7 @@ function FormComponent(props) {
1362
1572
  });
1363
1573
  }
1364
1574
 
1365
- const type$4 = 'number';
1575
+ const type$5 = 'number';
1366
1576
  function Number(props) {
1367
1577
  const {
1368
1578
  disabled,
@@ -1383,10 +1593,11 @@ function Number(props) {
1383
1593
  const onChange = ({
1384
1594
  target
1385
1595
  }) => {
1386
- const parsedValue = parseInt(target.value, 10);
1387
1596
  props.onChange({
1388
1597
  field,
1389
- value: isNaN(parsedValue) ? null : parsedValue
1598
+ value: Number.sanitizeValue({
1599
+ value: target.value
1600
+ })
1390
1601
  });
1391
1602
  };
1392
1603
 
@@ -1394,7 +1605,7 @@ function Number(props) {
1394
1605
  formId
1395
1606
  } = useContext(FormContext);
1396
1607
  return jsxs("div", {
1397
- class: formFieldClasses(type$4, errors),
1608
+ class: formFieldClasses(type$5, errors),
1398
1609
  children: [jsx(Label, {
1399
1610
  id: prefixId(id, formId),
1400
1611
  label: label,
@@ -1419,12 +1630,19 @@ Number.create = function (options = {}) {
1419
1630
  };
1420
1631
  };
1421
1632
 
1422
- Number.type = type$4;
1633
+ Number.sanitizeValue = ({
1634
+ value
1635
+ }) => {
1636
+ const parsedValue = parseInt(value, 10);
1637
+ return isNaN(parsedValue) ? null : parsedValue;
1638
+ };
1639
+
1640
+ Number.type = type$5;
1423
1641
  Number.keyed = true;
1424
1642
  Number.label = 'Number';
1425
1643
  Number.emptyValue = null;
1426
1644
 
1427
- const type$3 = 'radio';
1645
+ const type$4 = 'radio';
1428
1646
  function Radio(props) {
1429
1647
  const {
1430
1648
  disabled,
@@ -1436,8 +1654,7 @@ function Radio(props) {
1436
1654
  description,
1437
1655
  id,
1438
1656
  label,
1439
- validate = {},
1440
- values
1657
+ validate = {}
1441
1658
  } = field;
1442
1659
  const {
1443
1660
  required
@@ -1450,26 +1667,30 @@ function Radio(props) {
1450
1667
  });
1451
1668
  };
1452
1669
 
1670
+ const {
1671
+ state: loadState,
1672
+ values: options
1673
+ } = useValuesAsync(field);
1453
1674
  const {
1454
1675
  formId
1455
1676
  } = useContext(FormContext);
1456
1677
  return jsxs("div", {
1457
- class: formFieldClasses(type$3, errors),
1678
+ class: formFieldClasses(type$4, errors),
1458
1679
  children: [jsx(Label, {
1459
1680
  label: label,
1460
1681
  required: required
1461
- }), values.map((v, index) => {
1682
+ }), loadState == LOAD_STATES.LOADED && options.map((option, index) => {
1462
1683
  return jsx(Label, {
1463
1684
  id: prefixId(`${id}-${index}`, formId),
1464
- label: v.label,
1685
+ label: option.label,
1465
1686
  required: false,
1466
1687
  children: jsx("input", {
1467
- checked: v.value === value,
1688
+ checked: option.value === value,
1468
1689
  class: "fjs-input",
1469
1690
  disabled: disabled,
1470
1691
  id: prefixId(`${id}-${index}`, formId),
1471
1692
  type: "radio",
1472
- onClick: () => onChange(v.value)
1693
+ onClick: () => onChange(option.value)
1473
1694
  })
1474
1695
  }, `${id}-${index}`);
1475
1696
  }), jsx(Description, {
@@ -1481,6 +1702,7 @@ function Radio(props) {
1481
1702
  }
1482
1703
 
1483
1704
  Radio.create = function (options = {}) {
1705
+ if (options.valuesKey) return options;
1484
1706
  return {
1485
1707
  values: [{
1486
1708
  label: 'Value',
@@ -1490,12 +1712,13 @@ Radio.create = function (options = {}) {
1490
1712
  };
1491
1713
  };
1492
1714
 
1493
- Radio.type = type$3;
1715
+ Radio.type = type$4;
1494
1716
  Radio.label = 'Radio';
1495
1717
  Radio.keyed = true;
1496
1718
  Radio.emptyValue = null;
1719
+ Radio.sanitizeValue = sanitizeSingleSelectValue;
1497
1720
 
1498
- const type$2 = 'select';
1721
+ const type$3 = 'select';
1499
1722
  function Select(props) {
1500
1723
  const {
1501
1724
  disabled,
@@ -1507,8 +1730,7 @@ function Select(props) {
1507
1730
  description,
1508
1731
  id,
1509
1732
  label,
1510
- validate = {},
1511
- values
1733
+ validate = {}
1512
1734
  } = field;
1513
1735
  const {
1514
1736
  required
@@ -1523,11 +1745,15 @@ function Select(props) {
1523
1745
  });
1524
1746
  };
1525
1747
 
1748
+ const {
1749
+ state: loadState,
1750
+ values: options
1751
+ } = useValuesAsync(field);
1526
1752
  const {
1527
1753
  formId
1528
1754
  } = useContext(FormContext);
1529
1755
  return jsxs("div", {
1530
- class: formFieldClasses(type$2, errors),
1756
+ class: formFieldClasses(type$3, errors),
1531
1757
  children: [jsx(Label, {
1532
1758
  id: prefixId(id, formId),
1533
1759
  label: label,
@@ -1540,10 +1766,10 @@ function Select(props) {
1540
1766
  value: value || '',
1541
1767
  children: [jsx("option", {
1542
1768
  value: ""
1543
- }), values.map((v, index) => {
1769
+ }), loadState == LOAD_STATES.LOADED && options.map((option, index) => {
1544
1770
  return jsx("option", {
1545
- value: v.value,
1546
- children: v.label
1771
+ value: option.value,
1772
+ children: option.label
1547
1773
  }, `${id}-${index}`);
1548
1774
  })]
1549
1775
  }), jsx(Description, {
@@ -1555,6 +1781,7 @@ function Select(props) {
1555
1781
  }
1556
1782
 
1557
1783
  Select.create = function (options = {}) {
1784
+ if (options.valuesKey) return options;
1558
1785
  return {
1559
1786
  values: [{
1560
1787
  label: 'Value',
@@ -1564,10 +1791,314 @@ Select.create = function (options = {}) {
1564
1791
  };
1565
1792
  };
1566
1793
 
1567
- Select.type = type$2;
1794
+ Select.type = type$3;
1568
1795
  Select.label = 'Select';
1569
1796
  Select.keyed = true;
1570
1797
  Select.emptyValue = null;
1798
+ Select.sanitizeValue = sanitizeSingleSelectValue;
1799
+
1800
+ 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); }
1801
+ var CloseIcon = (({
1802
+ styles = {},
1803
+ ...props
1804
+ }) => /*#__PURE__*/React.createElement("svg", _extends({
1805
+ width: "16",
1806
+ height: "16",
1807
+ fill: "none",
1808
+ xmlns: "http://www.w3.org/2000/svg"
1809
+ }, props), /*#__PURE__*/React.createElement("path", {
1810
+ fillRule: "evenodd",
1811
+ clipRule: "evenodd",
1812
+ 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",
1813
+ fill: "#000"
1814
+ })));
1815
+
1816
+ function useKeyDownAction(targetKey, action, listenerElement = window) {
1817
+ function downHandler({
1818
+ key
1819
+ }) {
1820
+ if (key === targetKey) {
1821
+ action();
1822
+ }
1823
+ }
1824
+
1825
+ useEffect(() => {
1826
+ listenerElement.addEventListener('keydown', downHandler);
1827
+ return () => {
1828
+ listenerElement.removeEventListener('keydown', downHandler);
1829
+ };
1830
+ });
1831
+ }
1832
+
1833
+ const DEFAULT_LABEL_GETTER = value => value;
1834
+
1835
+ const NOOP = () => {};
1836
+
1837
+ function DropdownList(props) {
1838
+ const {
1839
+ keyEventsListener = window,
1840
+ values = [],
1841
+ getLabel = DEFAULT_LABEL_GETTER,
1842
+ onValueSelected = NOOP,
1843
+ height = 235,
1844
+ emptyListMessage = 'No results'
1845
+ } = props;
1846
+ const [mouseControl, setMouseControl] = useState(true);
1847
+ const [focusedValueIndex, setFocusedValueIndex] = useState(0);
1848
+ const dropdownContainer = useRef();
1849
+ const mouseScreenPos = useRef();
1850
+ const focusedItem = useMemo(() => values.length ? values[focusedValueIndex] : null, [focusedValueIndex, values]);
1851
+ const changeFocusedValueIndex = useCallback(delta => {
1852
+ setFocusedValueIndex(x => Math.min(Math.max(0, x + delta), values.length - 1));
1853
+ }, [values.length]);
1854
+ useEffect(() => {
1855
+ if (focusedValueIndex === 0) return;
1856
+
1857
+ if (!focusedValueIndex || !values.length) {
1858
+ setFocusedValueIndex(0);
1859
+ } else if (focusedValueIndex >= values.length) {
1860
+ setFocusedValueIndex(values.length - 1);
1861
+ }
1862
+ }, [focusedValueIndex, values.length]);
1863
+ useKeyDownAction('ArrowUp', () => {
1864
+ if (values.length) {
1865
+ changeFocusedValueIndex(-1);
1866
+ setMouseControl(false);
1867
+ }
1868
+ }, keyEventsListener);
1869
+ useKeyDownAction('ArrowDown', () => {
1870
+ if (values.length) {
1871
+ changeFocusedValueIndex(1);
1872
+ setMouseControl(false);
1873
+ }
1874
+ }, keyEventsListener);
1875
+ useKeyDownAction('Enter', () => {
1876
+ if (focusedItem) {
1877
+ onValueSelected(focusedItem);
1878
+ }
1879
+ }, keyEventsListener);
1880
+ useEffect(() => {
1881
+ const individualEntries = dropdownContainer.current.children;
1882
+
1883
+ if (individualEntries.length && !mouseControl) {
1884
+ individualEntries[focusedValueIndex].scrollIntoView({
1885
+ block: 'nearest',
1886
+ inline: 'nearest'
1887
+ });
1888
+ }
1889
+ }, [focusedValueIndex, mouseControl]);
1890
+
1891
+ const mouseMove = (e, i) => {
1892
+ const userMoved = !mouseScreenPos.current || mouseScreenPos.current.x !== e.screenX && mouseScreenPos.current.y !== e.screenY;
1893
+
1894
+ if (userMoved) {
1895
+ mouseScreenPos.current = {
1896
+ x: e.screenX,
1897
+ y: e.screenY
1898
+ };
1899
+
1900
+ if (!mouseControl) {
1901
+ setMouseControl(true);
1902
+ setFocusedValueIndex(i);
1903
+ }
1904
+ }
1905
+ };
1906
+
1907
+ return jsxs("div", {
1908
+ ref: dropdownContainer,
1909
+ tabIndex: -1,
1910
+ class: "fjs-dropdownlist",
1911
+ style: {
1912
+ maxHeight: height
1913
+ },
1914
+ children: [!!values.length && values.map((v, i) => {
1915
+ return jsx("div", {
1916
+ class: 'fjs-dropdownlist-item' + (focusedValueIndex === i ? ' focused' : ''),
1917
+ onMouseMove: e => mouseMove(e, i),
1918
+ onMouseEnter: mouseControl ? () => setFocusedValueIndex(i) : undefined,
1919
+ onMouseDown: e => {
1920
+ e.preventDefault();
1921
+ onValueSelected(v);
1922
+ },
1923
+ children: getLabel(v)
1924
+ });
1925
+ }), !values.length && jsx("div", {
1926
+ class: "fjs-dropdownlist-empty",
1927
+ children: emptyListMessage
1928
+ })]
1929
+ });
1930
+ }
1931
+
1932
+ const type$2 = 'taglist';
1933
+ function Taglist(props) {
1934
+ const {
1935
+ disabled,
1936
+ errors = [],
1937
+ field,
1938
+ value: values = []
1939
+ } = props;
1940
+ const {
1941
+ description,
1942
+ id,
1943
+ label
1944
+ } = field;
1945
+ const {
1946
+ formId
1947
+ } = useContext(FormContext);
1948
+ const [filter, setFilter] = useState('');
1949
+ const [selectedValues, setSelectedValues] = useState([]);
1950
+ const [filteredValues, setFilteredValues] = useState([]);
1951
+ const [isDropdownExpanded, setIsDropdownExpanded] = useState(false);
1952
+ const [hasValuesLeft, setHasValuesLeft] = useState(true);
1953
+ const [isEscapeClosed, setIsEscapeClose] = useState(false);
1954
+ const searchbarRef = useRef();
1955
+ const {
1956
+ state: loadState,
1957
+ values: options
1958
+ } = 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
1959
+
1960
+ useEffect(() => {
1961
+ if (loadState === LOAD_STATES.LOADED) {
1962
+ const selectedValues = values.map(v => options.find(o => o.value === v)).filter(v => v !== undefined);
1963
+ setSelectedValues(selectedValues);
1964
+ } else {
1965
+ setSelectedValues([]);
1966
+ }
1967
+ }, [JSON.stringify(values), options, loadState]);
1968
+ useEffect(() => {
1969
+ if (loadState === LOAD_STATES.LOADED) {
1970
+ setFilteredValues(options.filter(o => o.label && o.value && o.label.toLowerCase().includes(filter.toLowerCase()) && !values.includes(o.value)));
1971
+ } else {
1972
+ setFilteredValues([]);
1973
+ }
1974
+ }, [filter, JSON.stringify(values), options]);
1975
+ useEffect(() => {
1976
+ setHasValuesLeft(selectedValues.length < options.length);
1977
+ }, [selectedValues.length, options.length]);
1978
+
1979
+ const onFilterChange = ({
1980
+ target
1981
+ }) => {
1982
+ setIsEscapeClose(false);
1983
+ setFilter(target.value);
1984
+ };
1985
+
1986
+ const selectValue = option => {
1987
+ setFilter('');
1988
+ props.onChange({
1989
+ value: [...values, option.value],
1990
+ field
1991
+ });
1992
+ };
1993
+
1994
+ const deselectValue = option => {
1995
+ props.onChange({
1996
+ value: values.filter(v => v != option.value),
1997
+ field
1998
+ });
1999
+ };
2000
+
2001
+ const onInputKeyDown = e => {
2002
+ switch (e.key) {
2003
+ case 'ArrowUp':
2004
+ case 'ArrowDown':
2005
+ // We do not want the cursor to seek in the search field when we press up and down
2006
+ e.preventDefault();
2007
+ break;
2008
+
2009
+ case 'Backspace':
2010
+ if (!filter && selectedValues.length) {
2011
+ deselectValue(selectedValues[selectedValues.length - 1]);
2012
+ }
2013
+
2014
+ break;
2015
+
2016
+ case 'Escape':
2017
+ setIsEscapeClose(true);
2018
+ break;
2019
+
2020
+ case 'Enter':
2021
+ if (isEscapeClosed) {
2022
+ setIsEscapeClose(false);
2023
+ }
2024
+
2025
+ break;
2026
+ }
2027
+ };
2028
+
2029
+ return jsxs("div", {
2030
+ class: formFieldClasses(type$2, errors),
2031
+ children: [jsx(Label, {
2032
+ label: label,
2033
+ id: prefixId(id, formId)
2034
+ }), jsxs("div", {
2035
+ class: classNames('fjs-taglist', {
2036
+ 'disabled': disabled
2037
+ }),
2038
+ children: [!disabled && loadState === LOAD_STATES.LOADED && selectedValues.map(sv => {
2039
+ return jsxs("div", {
2040
+ class: "fjs-taglist-tag",
2041
+ onMouseDown: e => e.preventDefault(),
2042
+ children: [jsx("span", {
2043
+ class: "fjs-taglist-tag-label",
2044
+ children: sv.label
2045
+ }), jsx("span", {
2046
+ class: "fjs-taglist-tag-remove",
2047
+ onMouseDown: () => deselectValue(sv),
2048
+ children: jsx(CloseIcon, {})
2049
+ })]
2050
+ });
2051
+ }), jsx("input", {
2052
+ disabled: disabled,
2053
+ class: "fjs-taglist-input",
2054
+ ref: searchbarRef,
2055
+ id: prefixId(`${id}-search`, formId),
2056
+ onChange: onFilterChange,
2057
+ type: "text",
2058
+ value: filter,
2059
+ placeholder: 'Search',
2060
+ autoComplete: "off",
2061
+ onKeyDown: e => onInputKeyDown(e),
2062
+ onMouseDown: () => setIsEscapeClose(false),
2063
+ onFocus: () => setIsDropdownExpanded(true),
2064
+ onBlur: () => {
2065
+ setIsDropdownExpanded(false);
2066
+ setFilter('');
2067
+ }
2068
+ })]
2069
+ }), jsx("div", {
2070
+ class: "fjs-taglist-anchor",
2071
+ children: !disabled && loadState === LOAD_STATES.LOADED && isDropdownExpanded && !isEscapeClosed && jsx(DropdownList, {
2072
+ values: filteredValues,
2073
+ getLabel: v => v.label,
2074
+ onValueSelected: v => selectValue(v),
2075
+ emptyListMessage: hasValuesLeft ? 'No results' : 'All values selected',
2076
+ listenerElement: searchbarRef.current
2077
+ })
2078
+ }), jsx(Description, {
2079
+ description: description
2080
+ }), jsx(Errors, {
2081
+ errors: errors
2082
+ })]
2083
+ });
2084
+ }
2085
+
2086
+ Taglist.create = function (options = {}) {
2087
+ if (options.valuesKey) return options;
2088
+ return {
2089
+ values: [{
2090
+ label: 'Value',
2091
+ value: 'value'
2092
+ }],
2093
+ ...options
2094
+ };
2095
+ };
2096
+
2097
+ Taglist.type = type$2;
2098
+ Taglist.label = 'Taglist';
2099
+ Taglist.keyed = true;
2100
+ Taglist.emptyValue = [];
2101
+ Taglist.sanitizeValue = sanitizeMultiSelectValue;
1571
2102
 
1572
2103
  const type$1 = 'text';
1573
2104
  function Text(props) {
@@ -1657,7 +2188,11 @@ Textfield.label = 'Text Field';
1657
2188
  Textfield.keyed = true;
1658
2189
  Textfield.emptyValue = '';
1659
2190
 
1660
- const formFields = [Button, Checkbox, Default, Number, Radio, Select, Text, Textfield];
2191
+ Textfield.sanitizeValue = ({
2192
+ value
2193
+ }) => isArray(value) || isObject(value) ? null : String(value);
2194
+
2195
+ const formFields = [Button, Checkbox, Checklist, Default, Number, Radio, Select, Taglist, Text, Textfield];
1661
2196
 
1662
2197
  class FormFields {
1663
2198
  constructor() {
@@ -2144,7 +2679,7 @@ class Form {
2144
2679
 
2145
2680
  }
2146
2681
 
2147
- const schemaVersion = 4;
2682
+ const schemaVersion = 5;
2148
2683
  /**
2149
2684
  * @typedef { import('./types').CreateFormOptions } CreateFormOptions
2150
2685
  */
@@ -2169,5 +2704,5 @@ function createForm(options) {
2169
2704
  });
2170
2705
  }
2171
2706
 
2172
- export { Button, Checkbox, Default, Form, FormComponent, FormContext, FormFieldRegistry, FormFields, FormRenderContext, Number, Radio, Select, Text, Textfield, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
2707
+ export { Button, Checkbox, Checklist, Default, Form, FormComponent, FormContext, FormFieldRegistry, FormFields, FormRenderContext, Number, Radio, Select, Taglist, Text, Textfield, clone, createForm, createFormContainer, createInjector, findErrors, formFields, generateIdForType, generateIndexForType, getSchemaVariables, isRequired, pathParse, pathStringify, pathsEqual, schemaVersion };
2173
2708
  //# sourceMappingURL=index.es.js.map