@bpmn-io/form-js-editor 0.12.1 → 0.13.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 (48) hide show
  1. package/README.md +0 -3
  2. package/dist/assets/form-js-editor-base.css +486 -0
  3. package/dist/assets/form-js-editor.css +1162 -60
  4. package/dist/index.cjs +1258 -529
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +1260 -531
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/types/FormEditor.d.ts +9 -0
  9. package/dist/types/core/FormFieldRegistry.d.ts +1 -1
  10. package/dist/types/core/FormLayoutValidator.d.ts +16 -0
  11. package/dist/types/core/FormLayouter.d.ts +1 -0
  12. package/dist/types/core/index.d.ts +8 -4
  13. package/dist/types/features/dragging/Dragging.d.ts +60 -0
  14. package/dist/types/features/dragging/index.d.ts +6 -0
  15. package/dist/types/features/editor-actions/index.d.ts +1 -1
  16. package/dist/types/features/expression-language/index.d.ts +8 -0
  17. package/dist/types/features/keyboard/index.d.ts +1 -1
  18. package/dist/types/features/modeling/FormLayoutUpdater.d.ts +13 -0
  19. package/dist/types/features/modeling/Modeling.d.ts +7 -7
  20. package/dist/types/features/modeling/behavior/index.d.ts +3 -3
  21. package/dist/types/features/modeling/cmd/Util.d.ts +1 -0
  22. package/dist/types/features/modeling/index.d.ts +3 -1
  23. package/dist/types/features/palette/index.d.ts +1 -1
  24. package/dist/types/features/properties-panel/entries/ColumnsEntry.d.ts +2 -1
  25. package/dist/types/features/properties-panel/entries/DefaultValueEntry.d.ts +1 -0
  26. package/dist/types/features/properties-panel/entries/InputKeyValuesSourceEntry.d.ts +1 -1
  27. package/dist/types/features/properties-panel/entries/SelectEntries.d.ts +1 -0
  28. package/dist/types/features/properties-panel/entries/factories/simpleBoolEntryFactory.d.ts +1 -0
  29. package/dist/types/features/properties-panel/groups/GeneralGroup.d.ts +1 -1
  30. package/dist/types/features/properties-panel/groups/LayoutGroup.d.ts +11 -0
  31. package/dist/types/features/properties-panel/groups/index.d.ts +1 -0
  32. package/dist/types/features/properties-panel/index.d.ts +1 -1
  33. package/dist/types/features/selection/index.d.ts +2 -2
  34. package/dist/types/import/Importer.d.ts +4 -2
  35. package/dist/types/import/index.d.ts +1 -1
  36. package/dist/types/index.d.ts +2 -2
  37. package/dist/types/render/EditorFormFields.d.ts +1 -1
  38. package/dist/types/render/components/FieldDragPreview.d.ts +1 -0
  39. package/dist/types/render/components/editor-form-fields/EditorText.d.ts +1 -1
  40. package/dist/types/render/components/editor-form-fields/index.d.ts +1 -1
  41. package/dist/types/render/components/icons/index.d.ts +1 -1
  42. package/dist/types/render/hooks/index.d.ts +3 -0
  43. package/dist/types/render/hooks/useDebounce.d.ts +1 -0
  44. package/dist/types/render/hooks/usePrevious.d.ts +1 -0
  45. package/dist/types/render/index.d.ts +2 -2
  46. package/package.json +10 -8
  47. package/dist/assets/dragula.css +0 -22
  48. package/dist/assets/properties-panel.css +0 -1016
package/dist/index.es.js CHANGED
@@ -1,15 +1,16 @@
1
- import { FormFieldRegistry as FormFieldRegistry$1, clone, iconsByType, isExpression, Text as Text$1, FormFields, formFields, FormContext, FormRenderContext, FormComponent, getSchemaVariables, Default, DATETIME_SUBTYPES, DATE_LABEL_PATH, TIME_LABEL_PATH, DATETIME_SUBTYPE_PATH, DATETIME_SUBTYPES_LABELS, TIME_SERIALISING_FORMAT_PATH, TIME_SERIALISING_FORMATS, TIME_INTERVAL_PATH, TIME_USE24H_PATH, DATE_DISALLOW_PAST_PATH, TIME_SERIALISINGFORMAT_LABELS, getValuesSource, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_PATHS, VALUES_SOURCES_LABELS, createFormContainer, createInjector, schemaVersion } from '@bpmn-io/form-js-viewer';
1
+ import { FormFieldRegistry as FormFieldRegistry$1, clone, iconsByType, Text as Text$1, FormFields, formFields, FormContext, FormRenderContext, FormComponent, FormLayouter, getSchemaVariables, DATETIME_SUBTYPES, DATE_LABEL_PATH, TIME_LABEL_PATH, DATETIME_SUBTYPE_PATH, DATETIME_SUBTYPES_LABELS, TIME_SERIALISING_FORMAT_PATH, TIME_SERIALISING_FORMATS, TIME_INTERVAL_PATH, TIME_USE24H_PATH, DATE_DISALLOW_PAST_PATH, TIME_SERIALISINGFORMAT_LABELS, getValuesSource, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_PATHS, VALUES_SOURCES_LABELS, FeelExpressionLanguage, FeelersTemplating, createFormContainer, createInjector, MarkdownModule, schemaVersion } from '@bpmn-io/form-js-viewer';
2
2
  export { schemaVersion } from '@bpmn-io/form-js-viewer';
3
3
  import Ids from 'ids';
4
4
  import { isArray, isFunction, isNumber, bind, assign, debounce, forEach, get, isObject, uniqueBy, sortBy, find, set as set$1, isUndefined, without, has, isString } from 'min-dash';
5
5
  import classnames from 'classnames';
6
- import { jsx, jsxs } from 'preact/jsx-runtime';
7
- import { createContext, render, createElement } from 'preact';
6
+ import { jsx, jsxs, Fragment } from 'preact/jsx-runtime';
8
7
  import { useContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from 'preact/hooks';
9
- import dragula from 'dragula';
8
+ import { createContext, render, createElement } from 'preact';
10
9
  import React, { forwardRef } from 'preact/compat';
10
+ import dragula from 'dragula';
11
11
  import { classes, closest, event, matches, domify, query } from 'min-dom';
12
12
  import { mutate } from 'array-move';
13
+ import { FeelersEditor } from 'feelers';
13
14
  import FeelEditor from '@bpmn-io/feel-editor';
14
15
  import Big from 'big.js';
15
16
 
@@ -569,19 +570,78 @@ class FormFieldRegistry extends FormFieldRegistry$1 {
569
570
  }
570
571
  }
571
572
 
573
+ class FormLayoutValidator {
574
+ /**
575
+ * @constructor
576
+ *
577
+ * @param { import('./FormLayouter').default } formLayouter
578
+ * @param { import('./FormFieldRegistry').default } formFieldRegistry
579
+ */
580
+ constructor(formLayouter, formFieldRegistry) {
581
+ this._formLayouter = formLayouter;
582
+ this._formFieldRegistry = formFieldRegistry;
583
+ }
584
+ validateField(field = {}, columns, row) {
585
+ // allow empty (auto columns)
586
+ if (columns) {
587
+ // allow minimum 2 cols
588
+ if (columns < 2) {
589
+ return 'Minimum 2 columns are allowed';
590
+ }
591
+
592
+ // allow maximum 16 cols
593
+ if (columns > 16) {
594
+ return 'Maximum 16 columns are allowed';
595
+ }
596
+ }
597
+ if (!row) {
598
+ row = this._formLayouter.getRowForField(field);
599
+ }
600
+
601
+ // calculate columns with and without updated field
602
+ let sumColumns = parseInt(columns) || 0;
603
+ let sumFields = 1;
604
+ let sumAutoCols = columns ? 0 : 1;
605
+ row.components.forEach(id => {
606
+ if (field.id === id) {
607
+ return;
608
+ }
609
+ const component = this._formFieldRegistry.get(id);
610
+ const cols = (component.layout || {}).columns;
611
+ if (!cols) {
612
+ sumAutoCols++;
613
+ }
614
+ sumColumns += parseInt(cols) || 0;
615
+ sumFields++;
616
+ });
617
+
618
+ // do not allow overflows
619
+ if (sumColumns > 16 || sumColumns === 16 && sumAutoCols > 0 || columns === 16 && sumFields > 1) {
620
+ return 'New value exceeds the maximum of 16 columns per row';
621
+ }
622
+ if (sumFields > 4) {
623
+ return 'Maximum 4 fields per row are allowed';
624
+ }
625
+ return null;
626
+ }
627
+ }
628
+ FormLayoutValidator.$inject = ['formLayouter', 'formFieldRegistry'];
629
+
572
630
  class Importer {
573
631
  /**
574
632
  * @constructor
575
633
  * @param { import('../core/FormFieldRegistry').default } formFieldRegistry
576
634
  * @param { import('../core/FieldFactory').default } fieldFactory
635
+ * @param { import('../core/FormLayouter').default } formLayouter
577
636
  */
578
- constructor(formFieldRegistry, fieldFactory) {
637
+ constructor(formFieldRegistry, fieldFactory, formLayouter) {
579
638
  this._formFieldRegistry = formFieldRegistry;
580
639
  this._fieldFactory = fieldFactory;
640
+ this._formLayouter = formLayouter;
581
641
  }
582
642
 
583
643
  /**
584
- * Import schema creating fields, attaching additional
644
+ * Import schema creating rows, fields, attaching additional
585
645
  * information to each field and adding fields to the
586
646
  * field registry.
587
647
  *
@@ -601,6 +661,7 @@ class Importer {
601
661
  const warnings = [];
602
662
  try {
603
663
  const importedSchema = this.importFormField(clone(schema));
664
+ this._formLayouter.calculateLayout(clone(importedSchema));
604
665
  return {
605
666
  schema: importedSchema,
606
667
  warnings
@@ -665,7 +726,7 @@ class Importer {
665
726
  });
666
727
  }
667
728
  }
668
- Importer.$inject = ['formFieldRegistry', 'fieldFactory'];
729
+ Importer.$inject = ['formFieldRegistry', 'fieldFactory', 'formLayouter'];
669
730
 
670
731
  var importModule = {
671
732
  importer: ['type', Importer]
@@ -682,11 +743,35 @@ function editorFormFieldClasses(type, {
682
743
  });
683
744
  }
684
745
 
685
- function _extends$2() { _extends$2 = 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$2.apply(this, arguments); }
746
+ const DragAndDropContext = createContext({
747
+ drake: null
748
+ });
749
+ var DragAndDropContext$1 = DragAndDropContext;
750
+
751
+ /**
752
+ * @param {string} type
753
+ * @param {boolean} [strict]
754
+ *
755
+ * @returns {any}
756
+ */
757
+ function getService$1(type, strict) {}
758
+ const FormEditorContext = createContext({
759
+ getService: getService$1
760
+ });
761
+ var FormEditorContext$1 = FormEditorContext;
762
+
763
+ function useService$1 (type, strict) {
764
+ const {
765
+ getService
766
+ } = useContext(FormEditorContext$1);
767
+ return getService(type, strict);
768
+ }
769
+
770
+ function _extends$3() { _extends$3 = 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$3.apply(this, arguments); }
686
771
  var CloseIcon = (({
687
772
  styles = {},
688
773
  ...props
689
- }) => /*#__PURE__*/React.createElement("svg", _extends$2({
774
+ }) => /*#__PURE__*/React.createElement("svg", _extends$3({
690
775
  width: "16",
691
776
  height: "16",
692
777
  fill: "currentColor",
@@ -697,6 +782,23 @@ var CloseIcon = (({
697
782
  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"
698
783
  })));
699
784
 
785
+ function _extends$2() { _extends$2 = 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$2.apply(this, arguments); }
786
+ var DraggableIcon = (({
787
+ styles = {},
788
+ ...props
789
+ }) => /*#__PURE__*/React.createElement("svg", _extends$2({
790
+ xmlns: "http://www.w3.org/2000/svg",
791
+ width: "16",
792
+ height: "16",
793
+ fill: "currentcolor",
794
+ viewBox: "0 0 32 32"
795
+ }, props), /*#__PURE__*/React.createElement("path", {
796
+ d: "M10 6h4v4h-4zm8 0h4v4h-4zm-8 8h4v4h-4zm8 0h4v4h-4zm-8 8h4v4h-4zm8 0h4v4h-4z"
797
+ }), /*#__PURE__*/React.createElement("path", {
798
+ d: "M0 0h32v32H0z",
799
+ fill: "none"
800
+ })));
801
+
700
802
  function _extends$1() { _extends$1 = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends$1.apply(this, arguments); }
701
803
  var SearchIcon = (({
702
804
  styles = {},
@@ -717,6 +819,8 @@ function EditorText(props) {
717
819
  text = ''
718
820
  } = props.field;
719
821
  const Icon = iconsByType('text');
822
+ const templating = useService$1('templating');
823
+ const expressionLanguage = useService$1('expressionLanguage');
720
824
  if (!text) {
721
825
  return jsx("div", {
722
826
  class: editorFormFieldClasses(type),
@@ -728,7 +832,7 @@ function EditorText(props) {
728
832
  })
729
833
  });
730
834
  }
731
- if (isExpression(text)) {
835
+ if (expressionLanguage.isExpression(text)) {
732
836
  return jsx("div", {
733
837
  class: editorFormFieldClasses(type),
734
838
  children: jsxs("div", {
@@ -739,6 +843,17 @@ function EditorText(props) {
739
843
  })
740
844
  });
741
845
  }
846
+ if (templating.isTemplate(text)) {
847
+ return jsx("div", {
848
+ class: editorFormFieldClasses(type),
849
+ children: jsxs("div", {
850
+ class: "fjs-form-field-placeholder",
851
+ children: [jsx(Icon, {
852
+ viewBox: "0 0 54 54"
853
+ }), "Text view is templated"]
854
+ })
855
+ });
856
+ }
742
857
  return jsx(Text$1, {
743
858
  ...props,
744
859
  disableLinks: true
@@ -759,30 +874,6 @@ class EditorFormFields extends FormFields {
759
874
  }
760
875
  }
761
876
 
762
- const DragAndDropContext = createContext({
763
- drake: null
764
- });
765
- var DragAndDropContext$1 = DragAndDropContext;
766
-
767
- /**
768
- * @param {string} type
769
- * @param {boolean} [strict]
770
- *
771
- * @returns {any}
772
- */
773
- function getService$1(type, strict) {}
774
- const FormEditorContext = createContext({
775
- getService: getService$1
776
- });
777
- var FormEditorContext$1 = FormEditorContext;
778
-
779
- function useService$1 (type, strict) {
780
- const {
781
- getService
782
- } = useContext(FormEditorContext$1);
783
- return getService(type, strict);
784
- }
785
-
786
877
  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); }
787
878
  var ListDeleteIcon = (({
788
879
  styles = {},
@@ -886,7 +977,7 @@ function Palette(props) {
886
977
  return jsxs("div", {
887
978
  class: "fjs-palette-field fjs-drag-copy fjs-no-drop",
888
979
  "data-field-type": type,
889
- title: `Create a ${label} element`,
980
+ title: `Create ${getIndefiniteArticle(type)} ${label} element`,
890
981
  children: [Icon ? jsx(Icon, {
891
982
  class: "fjs-palette-field-icon",
892
983
  width: "36",
@@ -925,6 +1016,12 @@ function groupEntries(entries) {
925
1016
  });
926
1017
  return groups.filter(g => g.entries.length);
927
1018
  }
1019
+ function getIndefiniteArticle(type) {
1020
+ if (['image'].includes(type)) {
1021
+ return 'an';
1022
+ }
1023
+ return 'a';
1024
+ }
928
1025
 
929
1026
  const CURSOR_CLS_PATTERN = /^fjs-cursor-.*$/;
930
1027
  function set(mode) {
@@ -938,6 +1035,307 @@ function unset() {
938
1035
  set(null);
939
1036
  }
940
1037
 
1038
+ const DRAG_CONTAINER_CLS = 'fjs-drag-container';
1039
+ const DROP_CONTAINER_VERTICAL_CLS = 'fjs-drop-container-vertical';
1040
+ const DROP_CONTAINER_HORIZONTAL_CLS = 'fjs-drop-container-horizontal';
1041
+ const DRAG_MOVE_CLS = 'fjs-drag-move';
1042
+ const DRAG_ROW_MOVE_CLS = 'fjs-drag-row-move';
1043
+ const DRAG_COPY_CLS = 'fjs-drag-copy';
1044
+ const DRAG_NO_DROP_CLS = 'fjs-no-drop';
1045
+ const DRAG_NO_MOVE_CLS = 'fjs-no-move';
1046
+ const ERROR_DROP_CLS = 'fjs-error-drop';
1047
+
1048
+ /**
1049
+ * @typedef { { id: String, components: Array<any> } } FormRow
1050
+ */
1051
+
1052
+ class Dragging {
1053
+ /**
1054
+ * @constructor
1055
+ *
1056
+ * @param { import('../../core/FormFieldRegistry').default } formFieldRegistry
1057
+ * @param { import('../../core/FormLayouter').default } formLayouter
1058
+ * @param { import('../../core/FormLayoutValidator').default } formLayoutValidator
1059
+ * @param { import('../../core/EventBus').default } eventBus
1060
+ * @param { import('../modeling/Modeling').default } modeling
1061
+ */
1062
+ constructor(formFieldRegistry, formLayouter, formLayoutValidator, eventBus, modeling) {
1063
+ this._formFieldRegistry = formFieldRegistry;
1064
+ this._formLayouter = formLayouter;
1065
+ this._formLayoutValidator = formLayoutValidator;
1066
+ this._eventBus = eventBus;
1067
+ this._modeling = modeling;
1068
+ }
1069
+
1070
+ /**
1071
+ * Calculcates position in form schema given the dropped place.
1072
+ *
1073
+ * @param { FormRow } targetRow
1074
+ * @param { any } targetFormField
1075
+ * @param { HTMLElement } sibling
1076
+ * @returns { number }
1077
+ */
1078
+ getTargetIndex(targetRow, targetFormField, sibling) {
1079
+ /** @type HTMLElement */
1080
+ const siblingFormFieldNode = sibling && sibling.querySelector('.fjs-element');
1081
+ const siblingFormField = siblingFormFieldNode && this._formFieldRegistry.get(siblingFormFieldNode.dataset.id);
1082
+
1083
+ // (1) dropped before existing field => place before
1084
+ if (siblingFormField) {
1085
+ return getFormFieldIndex$1(targetFormField, siblingFormField);
1086
+ }
1087
+
1088
+ // (2) dropped in row => place at the end of row (after last field in row)
1089
+ if (targetRow) {
1090
+ return getFormFieldIndex$1(targetFormField, this._formFieldRegistry.get(targetRow.components[targetRow.components.length - 1])) + 1;
1091
+ }
1092
+
1093
+ // (3) dropped as last item
1094
+ return targetFormField.components.length;
1095
+ }
1096
+ validateDrop(element, target) {
1097
+ const formFieldNode = element.querySelector('.fjs-element');
1098
+ const targetRow = this._formLayouter.getRow(target.dataset.rowId);
1099
+ let columns;
1100
+ let formField;
1101
+ if (formFieldNode) {
1102
+ formField = this._formFieldRegistry.get(formFieldNode.dataset.id);
1103
+ columns = (formField.layout || {}).columns;
1104
+ }
1105
+ return this._formLayoutValidator.validateField(formField, columns, targetRow);
1106
+ }
1107
+ moveField(element, source, targetRow, targetFormField, targetIndex) {
1108
+ const formFieldNode = element.querySelector('.fjs-element');
1109
+ const formField = this._formFieldRegistry.get(formFieldNode.dataset.id);
1110
+ const sourceParent = getFormParent(source);
1111
+ const sourceFormField = this._formFieldRegistry.get(sourceParent.dataset.id);
1112
+ const sourceIndex = getFormFieldIndex$1(sourceFormField, formField);
1113
+ const sourceRow = this._formLayouter.getRowForField(formField);
1114
+ this._modeling.moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex, sourceRow, targetRow);
1115
+ }
1116
+ createNewField(element, targetRow, targetFormField, targetIndex) {
1117
+ const type = element.dataset.fieldType;
1118
+ let attrs = {
1119
+ type
1120
+ };
1121
+ attrs = {
1122
+ ...attrs,
1123
+ layout: {
1124
+ row: targetRow ? targetRow.id : this._formLayouter.nextRowId(),
1125
+ // enable auto columns
1126
+ columns: null
1127
+ }
1128
+ };
1129
+ this._modeling.addFormField(attrs, targetFormField, targetIndex);
1130
+ }
1131
+ handleRowDrop(el, target, source, sibling) {
1132
+ const targetFormField = this._formFieldRegistry.get(target.dataset.id);
1133
+ const rowNode = el.querySelector('.fjs-layout-row');
1134
+ const row = this._formLayouter.getRow(rowNode.dataset.rowId);
1135
+
1136
+ // move each field in the row before first field of sibling row
1137
+ row.components.forEach((id, index) => {
1138
+ const formField = this._formFieldRegistry.get(id);
1139
+ const sourceParent = getFormParent(source);
1140
+ const sourceFormField = this._formFieldRegistry.get(sourceParent.dataset.id);
1141
+ const siblingRowNode = sibling && sibling.querySelector('.fjs-layout-row');
1142
+ const siblingRow = siblingRowNode && this._formLayouter.getRow(siblingRowNode.dataset.rowId);
1143
+ const siblingFormField = sibling && this._formFieldRegistry.get(siblingRow.components[0]);
1144
+ const sourceIndex = getFormFieldIndex$1(sourceFormField, formField);
1145
+ const targetIndex = (siblingRowNode ? getFormFieldIndex$1(targetFormField, siblingFormField) : targetFormField.components.length) + index;
1146
+ this._modeling.moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex, row, row);
1147
+ });
1148
+ }
1149
+ handleElementDrop(el, target, source, sibling, drake) {
1150
+ // (1) detect drop target
1151
+ const targetFormField = this._formFieldRegistry.get(getFormParent(target).dataset.id);
1152
+ let targetRow;
1153
+
1154
+ // (2.1) dropped in existing row
1155
+ if (isRow(target)) {
1156
+ unsetDropNotAllowed(target);
1157
+ targetRow = this._formLayouter.getRow(target.dataset.rowId);
1158
+
1159
+ // validate whether drop is allowed
1160
+ const validationError = this.validateDrop(el, target);
1161
+ if (validationError) {
1162
+ return drake.cancel(true);
1163
+ }
1164
+ }
1165
+ drake.remove();
1166
+
1167
+ // (3) detect position to drop field in schema order
1168
+ const targetIndex = this.getTargetIndex(targetRow, targetFormField, sibling);
1169
+
1170
+ // (4) create new field or move existing
1171
+ if (isPalette(source)) {
1172
+ this.createNewField(el, targetRow, targetFormField, targetIndex);
1173
+ } else {
1174
+ this.moveField(el, source, targetRow, targetFormField, targetIndex);
1175
+ }
1176
+ }
1177
+
1178
+ /**
1179
+ * @param { { container: Array<string>, direction: string, mirrorContainer: string } } options
1180
+ */
1181
+ createDragulaInstance(options) {
1182
+ const {
1183
+ container,
1184
+ direction,
1185
+ mirrorContainer
1186
+ } = options || {};
1187
+ const dragulaInstance = dragula({
1188
+ direction,
1189
+ mirrorContainer,
1190
+ isContainer(el) {
1191
+ return container.some(cls => el.classList.contains(cls));
1192
+ },
1193
+ moves(el, source, handle) {
1194
+ return !handle.classList.contains(DRAG_NO_MOVE_CLS) && (el.classList.contains(DRAG_MOVE_CLS) || el.classList.contains(DRAG_COPY_CLS) || el.classList.contains(DRAG_ROW_MOVE_CLS));
1195
+ },
1196
+ copy(el) {
1197
+ return el.classList.contains(DRAG_COPY_CLS);
1198
+ },
1199
+ accepts: (el, target) => {
1200
+ unsetDropNotAllowed(target);
1201
+
1202
+ // allow dropping rows only between rows
1203
+ if (el.classList.contains(DRAG_ROW_MOVE_CLS)) {
1204
+ return !target.classList.contains(DROP_CONTAINER_HORIZONTAL_CLS);
1205
+ }
1206
+
1207
+ // validate field drop in row
1208
+ if (isRow(target)) {
1209
+ const validationError = this.validateDrop(el, target);
1210
+ if (validationError) {
1211
+ // set error feedback to row
1212
+ setDropNotAllowed(target);
1213
+ }
1214
+ }
1215
+ return !target.classList.contains(DRAG_NO_DROP_CLS);
1216
+ },
1217
+ slideFactorX: 10,
1218
+ slideFactorY: 5
1219
+ });
1220
+
1221
+ // bind life cycle events
1222
+ dragulaInstance.on('drag', (element, source) => {
1223
+ this.emit('drag.start', {
1224
+ element,
1225
+ source
1226
+ });
1227
+ });
1228
+ dragulaInstance.on('dragend', element => {
1229
+ this.emit('drag.end', {
1230
+ element
1231
+ });
1232
+ });
1233
+ dragulaInstance.on('drop', (element, target, source, sibling) => {
1234
+ this.emit('drag.drop', {
1235
+ element,
1236
+ target,
1237
+ source,
1238
+ sibling
1239
+ });
1240
+ });
1241
+ dragulaInstance.on('over', (element, container, source) => {
1242
+ this.emit('drag.hover', {
1243
+ element,
1244
+ container,
1245
+ source
1246
+ });
1247
+ });
1248
+ dragulaInstance.on('out', (element, container, source) => {
1249
+ this.emit('drag.out', {
1250
+ element,
1251
+ container,
1252
+ source
1253
+ });
1254
+ });
1255
+ dragulaInstance.on('cancel', (element, container, source) => {
1256
+ this.emit('drag.cancel', {
1257
+ element,
1258
+ container,
1259
+ source
1260
+ });
1261
+ });
1262
+ dragulaInstance.on('drop', (el, target, source, sibling) => {
1263
+ if (!target) {
1264
+ dragulaInstance.remove();
1265
+ return;
1266
+ }
1267
+
1268
+ // (1) handle row drop
1269
+ if (isDragRow(el)) {
1270
+ this.handleRowDrop(el, target, source, sibling);
1271
+ } else {
1272
+ // (2) handle form field drop
1273
+ this.handleElementDrop(el, target, source, sibling, dragulaInstance);
1274
+ }
1275
+ });
1276
+ this.emit('dragula.created', dragulaInstance);
1277
+ return dragulaInstance;
1278
+ }
1279
+ emit(event, context) {
1280
+ this._eventBus.fire(event, context);
1281
+ }
1282
+ }
1283
+ Dragging.$inject = ['formFieldRegistry', 'formLayouter', 'formLayoutValidator', 'eventBus', 'modeling'];
1284
+
1285
+ // helper //////////
1286
+
1287
+ function getFormFieldIndex$1(parent, formField) {
1288
+ let fieldFormIndex = parent.components.length;
1289
+ parent.components.forEach(({
1290
+ id
1291
+ }, index) => {
1292
+ if (id === formField.id) {
1293
+ fieldFormIndex = index;
1294
+ }
1295
+ });
1296
+ return fieldFormIndex;
1297
+ }
1298
+ function isRow(node) {
1299
+ return node.classList.contains('fjs-layout-row');
1300
+ }
1301
+ function isDragRow(node) {
1302
+ return node.classList.contains(DRAG_ROW_MOVE_CLS);
1303
+ }
1304
+ function isPalette(node) {
1305
+ return node.classList.contains('fjs-palette-fields');
1306
+ }
1307
+ function getFormParent(node) {
1308
+ return node.closest('.fjs-element');
1309
+ }
1310
+ function setDropNotAllowed(node) {
1311
+ node.classList.add(ERROR_DROP_CLS);
1312
+ set('not-allowed');
1313
+ }
1314
+ function unsetDropNotAllowed(node) {
1315
+ node.classList.remove(ERROR_DROP_CLS);
1316
+ set('grabbing');
1317
+ }
1318
+
1319
+ function FieldDragPreview(props) {
1320
+ const {
1321
+ class: className,
1322
+ Icon,
1323
+ label
1324
+ } = props;
1325
+ return jsxs("div", {
1326
+ class: classnames('fjs-field-preview', className),
1327
+ children: [jsx(Icon, {
1328
+ class: "fjs-field-preview-icon",
1329
+ width: "36",
1330
+ height: "36",
1331
+ viewBox: "0 0 54 54"
1332
+ }), jsx("span", {
1333
+ class: "fjs-field-preview-text",
1334
+ children: label
1335
+ })]
1336
+ });
1337
+ }
1338
+
941
1339
  function ContextPad(props) {
942
1340
  if (!props.children) {
943
1341
  return null;
@@ -984,7 +1382,7 @@ function Element(props) {
984
1382
  event.stopPropagation();
985
1383
  selection.toggle(field);
986
1384
  }
987
- const classes = ['fjs-element'];
1385
+ const classes = [];
988
1386
  if (props.class) {
989
1387
  classes.push(...props.class.split(' '));
990
1388
  }
@@ -1003,7 +1401,9 @@ function Element(props) {
1003
1401
  "data-field-type": type,
1004
1402
  onClick: onClick,
1005
1403
  ref: ref,
1006
- children: [jsx(ContextPad, {
1404
+ children: [jsx(DebugColumns, {
1405
+ field: field
1406
+ }), jsx(ContextPad, {
1007
1407
  children: selection.isSelected(field) && field.type !== 'default' ? jsx("button", {
1008
1408
  class: "fjs-context-pad-item",
1009
1409
  onClick: onRemove,
@@ -1012,6 +1412,20 @@ function Element(props) {
1012
1412
  }), props.children]
1013
1413
  });
1014
1414
  }
1415
+ function DebugColumns(props) {
1416
+ const {
1417
+ field
1418
+ } = props;
1419
+ const debugColumnsConfig = useService$1('config.debugColumns');
1420
+ if (!debugColumnsConfig || field.type == 'default') {
1421
+ return null;
1422
+ }
1423
+ return jsx("div", {
1424
+ style: "width: fit-content; padding: 2px 6px; height: 16px; background: var(--color-blue-205-100-95); display: flex; justify-content: center; align-items: center; position: absolute; bottom: -2px; z-index: 2; font-size: 10px; right: 3px;",
1425
+ class: "fjs-debug-columns",
1426
+ children: (field.layout || {}).columns || 'auto'
1427
+ });
1428
+ }
1015
1429
  function Children(props) {
1016
1430
  const {
1017
1431
  field
@@ -1019,7 +1433,7 @@ function Children(props) {
1019
1433
  const {
1020
1434
  id
1021
1435
  } = field;
1022
- const classes = ['fjs-children', 'fjs-drag-container'];
1436
+ const classes = ['fjs-children', DROP_CONTAINER_VERTICAL_CLS];
1023
1437
  if (props.class) {
1024
1438
  classes.push(...props.class.split(' '));
1025
1439
  }
@@ -1029,12 +1443,51 @@ function Children(props) {
1029
1443
  children: props.children
1030
1444
  });
1031
1445
  }
1446
+ function Row(props) {
1447
+ const {
1448
+ row
1449
+ } = props;
1450
+ const {
1451
+ id
1452
+ } = row;
1453
+ const classes = [DROP_CONTAINER_HORIZONTAL_CLS];
1454
+ if (props.class) {
1455
+ classes.push(...props.class.split(' '));
1456
+ }
1457
+ return jsxs("div", {
1458
+ class: classnames(DRAG_ROW_MOVE_CLS),
1459
+ children: [jsx("span", {
1460
+ class: "fjs-row-dragger",
1461
+ children: jsx(DraggableIcon, {})
1462
+ }), jsx("div", {
1463
+ class: classes.join(' '),
1464
+ "data-row-id": id,
1465
+ children: props.children
1466
+ })]
1467
+ });
1468
+ }
1469
+ function Column(props) {
1470
+ const {
1471
+ field
1472
+ } = props;
1473
+ const classes = [DRAG_MOVE_CLS];
1474
+ if (field.type === 'default') {
1475
+ return props.children;
1476
+ }
1477
+ if (props.class) {
1478
+ classes.push(...props.class.split(' '));
1479
+ }
1480
+ return jsx("div", {
1481
+ "data-field-type": field.type,
1482
+ class: classes.join(' '),
1483
+ children: props.children
1484
+ });
1485
+ }
1032
1486
  function FormEditor$1(props) {
1033
- const eventBus = useService$1('eventBus'),
1487
+ const dragging = useService$1('dragging'),
1488
+ eventBus = useService$1('eventBus'),
1034
1489
  formEditor = useService$1('formEditor'),
1035
- formFieldRegistry = useService$1('formFieldRegistry'),
1036
1490
  injector = useService$1('injector'),
1037
- modeling = useService$1('modeling'),
1038
1491
  selection = useService$1('selection'),
1039
1492
  palette = useService$1('palette'),
1040
1493
  paletteConfig = useService$1('config.palette'),
@@ -1043,6 +1496,7 @@ function FormEditor$1(props) {
1043
1496
  const {
1044
1497
  schema
1045
1498
  } = formEditor._getState();
1499
+ const formContainerRef = useRef(null);
1046
1500
  const paletteRef = useRef(null);
1047
1501
  const propertiesPanelRef = useRef(null);
1048
1502
  const [, setSelection] = useState(schema);
@@ -1061,98 +1515,12 @@ function FormEditor$1(props) {
1061
1515
  drake
1062
1516
  };
1063
1517
  useEffect(() => {
1064
- const handleDragEvent = (type, context) => {
1065
- return eventBus.fire(type, context);
1066
- };
1067
- const createDragulaInstance = () => {
1068
- const dragulaInstance = dragula({
1069
- isContainer(el) {
1070
- return el.classList.contains('fjs-drag-container');
1071
- },
1072
- copy(el) {
1073
- return el.classList.contains('fjs-drag-copy');
1074
- },
1075
- accepts(el, target) {
1076
- return !target.classList.contains('fjs-no-drop');
1077
- },
1078
- slideFactorX: 10,
1079
- slideFactorY: 5
1080
- });
1081
-
1082
- // bind life cycle events
1083
- dragulaInstance.on('drag', (element, source) => {
1084
- handleDragEvent('drag.start', {
1085
- element,
1086
- source
1087
- });
1088
- });
1089
- dragulaInstance.on('dragend', element => {
1090
- handleDragEvent('drag.end', {
1091
- element
1092
- });
1093
- });
1094
- dragulaInstance.on('drop', (element, target, source, sibling) => {
1095
- handleDragEvent('drag.drop', {
1096
- element,
1097
- target,
1098
- source,
1099
- sibling
1100
- });
1101
- });
1102
- dragulaInstance.on('over', (element, container, source) => {
1103
- handleDragEvent('drag.hover', {
1104
- element,
1105
- container,
1106
- source
1107
- });
1108
- });
1109
- dragulaInstance.on('out', (element, container, source) => {
1110
- handleDragEvent('drag.out', {
1111
- element,
1112
- container,
1113
- source
1114
- });
1115
- });
1116
- dragulaInstance.on('cancel', (element, container, source) => {
1117
- handleDragEvent('drag.cancel', {
1118
- element,
1119
- container,
1120
- source
1121
- });
1122
- });
1123
-
1124
- // set custom styling
1125
- dragulaInstance.on('drag', () => {
1126
- set('grabbing');
1127
- });
1128
- dragulaInstance.on('dragend', () => {
1129
- unset();
1130
- });
1131
- dragulaInstance.on('drop', (el, target, source, sibling) => {
1132
- dragulaInstance.remove();
1133
- if (!target) {
1134
- return;
1135
- }
1136
- const targetFormField = formFieldRegistry.get(target.dataset.id);
1137
- const siblingFormField = sibling && formFieldRegistry.get(sibling.dataset.id),
1138
- targetIndex = siblingFormField ? getFormFieldIndex(targetFormField, siblingFormField) : targetFormField.components.length;
1139
- if (source.classList.contains('fjs-palette-fields')) {
1140
- const type = el.dataset.fieldType;
1141
- modeling.addFormField({
1142
- type
1143
- }, targetFormField, targetIndex);
1144
- } else {
1145
- const formField = formFieldRegistry.get(el.dataset.id),
1146
- sourceFormField = formFieldRegistry.get(source.dataset.id),
1147
- sourceIndex = getFormFieldIndex(sourceFormField, formField);
1148
- modeling.moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex);
1149
- }
1150
- });
1151
- eventBus.fire('dragula.created');
1152
- setDrake(dragulaInstance);
1153
- return dragulaInstance;
1154
- };
1155
- let dragulaInstance = createDragulaInstance();
1518
+ let dragulaInstance = dragging.createDragulaInstance({
1519
+ container: [DRAG_CONTAINER_CLS, DROP_CONTAINER_VERTICAL_CLS, DROP_CONTAINER_HORIZONTAL_CLS],
1520
+ direction: 'vertical',
1521
+ mirrorContainer: formContainerRef.current
1522
+ });
1523
+ setDrake(dragulaInstance);
1156
1524
  const onDetach = () => {
1157
1525
  if (dragulaInstance) {
1158
1526
  dragulaInstance.destroy();
@@ -1161,14 +1529,34 @@ function FormEditor$1(props) {
1161
1529
  };
1162
1530
  const onAttach = () => {
1163
1531
  onDetach();
1164
- dragulaInstance = createDragulaInstance();
1532
+ dragulaInstance = dragging.createDragulaInstance({
1533
+ container: [DRAG_CONTAINER_CLS, DROP_CONTAINER_VERTICAL_CLS, DROP_CONTAINER_HORIZONTAL_CLS],
1534
+ direction: 'vertical',
1535
+ mirrorContainer: formContainerRef.current
1536
+ });
1537
+ setDrake(dragulaInstance);
1538
+ };
1539
+ const onCreate = drake => {
1540
+ setDrake(drake);
1541
+ };
1542
+ const onDragStart = () => {
1543
+ set('grabbing');
1544
+ };
1545
+ const onDragEnd = () => {
1546
+ unset();
1165
1547
  };
1166
1548
  eventBus.on('attach', onAttach);
1167
1549
  eventBus.on('detach', onDetach);
1550
+ eventBus.on('dragula.created', onCreate);
1551
+ eventBus.on('drag.start', onDragStart);
1552
+ eventBus.on('drag.end', onDragEnd);
1168
1553
  return () => {
1169
1554
  onDetach();
1170
1555
  eventBus.off('attach', onAttach);
1171
1556
  eventBus.off('detach', onDetach);
1557
+ eventBus.off('dragula.created', onCreate);
1558
+ eventBus.off('drag.start', onDragStart);
1559
+ eventBus.off('drag.end', onDragEnd);
1172
1560
  };
1173
1561
  }, []);
1174
1562
 
@@ -1178,8 +1566,10 @@ function FormEditor$1(props) {
1178
1566
  }, []);
1179
1567
  const formRenderContext = {
1180
1568
  Children,
1569
+ Column,
1181
1570
  Element,
1182
- Empty
1571
+ Empty,
1572
+ Row
1183
1573
  };
1184
1574
  const formContext = {
1185
1575
  getService(type, strict = true) {
@@ -1228,6 +1618,7 @@ function FormEditor$1(props) {
1228
1618
  class: "fjs-editor-palette-container",
1229
1619
  ref: paletteRef
1230
1620
  }), jsx("div", {
1621
+ ref: formContainerRef,
1231
1622
  class: "fjs-form-container",
1232
1623
  children: jsx(FormContext.Provider, {
1233
1624
  value: formContext,
@@ -1263,29 +1654,39 @@ function CreatePreview(props) {
1263
1654
  } = useContext(DragAndDropContext$1);
1264
1655
  function handleCloned(clone, original, type) {
1265
1656
  const fieldType = clone.dataset.fieldType;
1266
- const Icon = iconsByType(fieldType);
1267
- const {
1268
- label
1269
- } = findPaletteEntry(fieldType);
1657
+
1658
+ // (1) field preview
1270
1659
  if (fieldType) {
1660
+ const {
1661
+ label
1662
+ } = findPaletteEntry(fieldType);
1663
+ const Icon = iconsByType(fieldType);
1271
1664
  clone.innerHTML = '';
1272
1665
  clone.class = 'gu-mirror';
1666
+ clone.classList.add('fjs-field-preview-container');
1273
1667
  if (original.classList.contains('fjs-palette-field')) {
1274
- render(jsxs("div", {
1275
- class: "fjs-palette-field",
1276
- children: [jsx(Icon, {
1277
- class: "fjs-palette-field-icon",
1278
- width: "36",
1279
- height: "36",
1280
- viewBox: "0 0 54 54"
1281
- }), jsx("span", {
1282
- class: "fjs-palette-field-text",
1283
- children: label
1284
- })]
1285
- }), clone);
1286
- } else {
1287
- render(jsx(Icon, {}), clone);
1668
+ // default to auto columns when creating from palette
1669
+ clone.classList.add('cds--col');
1288
1670
  }
1671
+
1672
+ // todo(pinussilvestrus): dragula, how to mitigate cursor position
1673
+ // https://github.com/bevacqua/dragula/issues/285
1674
+ render(jsx(FieldDragPreview, {
1675
+ label: label,
1676
+ Icon: Icon
1677
+ }), clone);
1678
+ } else {
1679
+ // (2) row preview
1680
+
1681
+ // remove elements from copy (context pad, row dragger, ...)
1682
+ ['fjs-context-pad', 'fjs-row-dragger', 'fjs-debug-columns'].forEach(cls => {
1683
+ const cloneNode = clone.querySelectorAll('.' + cls);
1684
+ cloneNode.length && cloneNode.forEach(e => e.remove());
1685
+ });
1686
+
1687
+ // mirror grid
1688
+ clone.classList.add('cds--grid');
1689
+ clone.classList.add('cds--grid--condensed');
1289
1690
  }
1290
1691
  }
1291
1692
  useEffect(() => {
@@ -1358,10 +1759,12 @@ var renderModule = {
1358
1759
 
1359
1760
  var core = {
1360
1761
  __depends__: [importModule, renderModule],
1762
+ debounce: ['factory', DebounceFactory],
1361
1763
  eventBus: ['type', EventBus],
1362
1764
  formFieldRegistry: ['type', FormFieldRegistry],
1363
- fieldFactory: ['type', FieldFactory],
1364
- debounce: ['factory', DebounceFactory]
1765
+ formLayouter: ['type', FormLayouter],
1766
+ formLayoutValidator: ['type', FormLayoutValidator],
1767
+ fieldFactory: ['type', FieldFactory]
1365
1768
  };
1366
1769
 
1367
1770
  var NOT_REGISTERED_ERROR = 'is not a registered action',
@@ -1639,6 +2042,11 @@ var EditorActionsModule = {
1639
2042
  editorActions: ['type', FormEditorActions]
1640
2043
  };
1641
2044
 
2045
+ var DraggingModule = {
2046
+ __init__: ['dragging'],
2047
+ dragging: ['type', Dragging]
2048
+ };
2049
+
1642
2050
  var KEYS_COPY = ['c', 'C', 'KeyC'];
1643
2051
  var KEYS_PASTE = ['v', 'V', 'KeyV'];
1644
2052
  var KEYS_REDO$1 = ['y', 'Y', 'KeyY'];
@@ -2051,6 +2459,13 @@ function updatePath(formFieldRegistry, formField, index) {
2051
2459
  formField._path = [...parent._path, 'components', index];
2052
2460
  return formField;
2053
2461
  }
2462
+ function updateRow(formField, rowId) {
2463
+ formField.layout = {
2464
+ ...(formField.layout || {}),
2465
+ row: rowId
2466
+ };
2467
+ return formField;
2468
+ }
2054
2469
 
2055
2470
  class AddFormFieldHandler {
2056
2471
  /**
@@ -2200,13 +2615,17 @@ class MoveFormFieldHandler {
2200
2615
  sourceFormField,
2201
2616
  targetFormField,
2202
2617
  sourceIndex,
2203
- targetIndex
2618
+ targetIndex,
2619
+ sourceRow,
2620
+ targetRow
2204
2621
  } = context;
2205
2622
  this.moveFormField({
2206
2623
  sourceFormField: targetFormField,
2207
2624
  targetFormField: sourceFormField,
2208
2625
  sourceIndex: targetIndex,
2209
- targetIndex: sourceIndex
2626
+ targetIndex: sourceIndex,
2627
+ sourceRow: targetRow,
2628
+ targetRow: sourceRow
2210
2629
  }, true);
2211
2630
  }
2212
2631
  moveFormField(context, revert) {
@@ -2214,7 +2633,8 @@ class MoveFormFieldHandler {
2214
2633
  sourceFormField,
2215
2634
  targetFormField,
2216
2635
  sourceIndex,
2217
- targetIndex
2636
+ targetIndex,
2637
+ targetRow
2218
2638
  } = context;
2219
2639
  let {
2220
2640
  schema
@@ -2230,11 +2650,15 @@ class MoveFormFieldHandler {
2230
2650
  targetIndex--;
2231
2651
  }
2232
2652
  }
2653
+ const formField = get(schema, [...sourcePath, sourceIndex]);
2654
+
2655
+ // (1) Add to row
2656
+ updateRow(formField, targetRow ? targetRow.id : null);
2233
2657
 
2234
- // (1) Move form field
2658
+ // (2) Move form field
2235
2659
  mutate(get(schema, sourcePath), sourceIndex, targetIndex);
2236
2660
 
2237
- // (2) Update paths of new form field and its siblings
2661
+ // (3) Update paths of new form field and its siblings
2238
2662
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
2239
2663
  } else {
2240
2664
  const formField = get(schema, [...sourcePath, sourceIndex]);
@@ -2247,10 +2671,13 @@ class MoveFormFieldHandler {
2247
2671
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
2248
2672
  const targetPath = [...targetFormField._path, 'components'];
2249
2673
 
2250
- // (3) Add form field
2674
+ // (3) Add to row
2675
+ updateRow(formField, targetRow ? targetRow.id : null);
2676
+
2677
+ // (4) Add form field
2251
2678
  arrayAdd$1(get(schema, targetPath), targetIndex, formField);
2252
2679
 
2253
- // (4) Update paths of siblings
2680
+ // (5) Update paths of siblings
2254
2681
  get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
2255
2682
  }
2256
2683
 
@@ -2442,13 +2869,15 @@ class Modeling {
2442
2869
  };
2443
2870
  this._commandStack.execute('formField.edit', context);
2444
2871
  }
2445
- moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex) {
2872
+ moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex, sourceRow, targetRow) {
2446
2873
  const context = {
2447
2874
  formField,
2448
2875
  sourceFormField,
2449
2876
  targetFormField,
2450
2877
  sourceIndex,
2451
- targetIndex
2878
+ targetIndex,
2879
+ sourceRow,
2880
+ targetRow
2452
2881
  };
2453
2882
  this._commandStack.execute('formField.move', context);
2454
2883
  }
@@ -2607,6 +3036,44 @@ forEach(hooks, function (hook) {
2607
3036
  };
2608
3037
  });
2609
3038
 
3039
+ class FormLayoutUpdater extends CommandInterceptor {
3040
+ constructor(eventBus, formLayouter, modeling, formEditor) {
3041
+ super(eventBus);
3042
+ this._eventBus = eventBus;
3043
+ this._formLayouter = formLayouter;
3044
+ this._modeling = modeling;
3045
+ this._formEditor = formEditor;
3046
+
3047
+ // @ts-ignore
3048
+ this.preExecute(['formField.add', 'formField.remove', 'formField.move', 'id.updateClaim'], event => this.updateRowIds(event));
3049
+
3050
+ // we need that as the state got updates
3051
+ // on the next tick (not in post execute)
3052
+ eventBus.on('changed', context => {
3053
+ const {
3054
+ schema
3055
+ } = context;
3056
+ this.updateLayout(schema);
3057
+ });
3058
+ }
3059
+ updateLayout(schema) {
3060
+ this._formLayouter.clear();
3061
+ this._formLayouter.calculateLayout(clone(schema));
3062
+ }
3063
+ updateRowIds(event) {
3064
+ const {
3065
+ schema
3066
+ } = this._formEditor._getState();
3067
+
3068
+ // make sure rows are persisted in schema (e.g. for migration case)
3069
+ schema.components.forEach(formField => {
3070
+ const row = this._formLayouter.getRowForField(formField);
3071
+ updateRow(formField, row.id);
3072
+ });
3073
+ }
3074
+ }
3075
+ FormLayoutUpdater.$inject = ['eventBus', 'formLayouter', 'modeling', 'formEditor'];
3076
+
2610
3077
  class IdBehavior extends CommandInterceptor {
2611
3078
  constructor(eventBus, modeling) {
2612
3079
  super(eventBus);
@@ -3128,7 +3595,8 @@ var commandModule = {
3128
3595
 
3129
3596
  var ModelingModule = {
3130
3597
  __depends__: [behaviorModule, commandModule],
3131
- __init__: ['modeling'],
3598
+ __init__: ['formLayoutUpdater', 'modeling'],
3599
+ formLayoutUpdater: ['type', FormLayoutUpdater],
3132
3600
  modeling: ['type', Modeling]
3133
3601
  };
3134
3602
 
@@ -3424,19 +3892,19 @@ const ErrorsContext = createContext({
3424
3892
  errors: {}
3425
3893
  });
3426
3894
 
3427
- /**
3428
- * @typedef {Function} <propertiesPanel.showEntry> callback
3429
- *
3430
- * @example
3431
- *
3432
- * useEvent('propertiesPanel.showEntry', ({ focus = false, ...rest }) => {
3433
- * // ...
3434
- * });
3435
- *
3436
- * @param {Object} context
3437
- * @param {boolean} [context.focus]
3438
- *
3439
- * @returns void
3895
+ /**
3896
+ * @typedef {Function} <propertiesPanel.showEntry> callback
3897
+ *
3898
+ * @example
3899
+ *
3900
+ * useEvent('propertiesPanel.showEntry', ({ focus = false, ...rest }) => {
3901
+ * // ...
3902
+ * });
3903
+ *
3904
+ * @param {Object} context
3905
+ * @param {boolean} [context.focus]
3906
+ *
3907
+ * @returns void
3440
3908
  */
3441
3909
  const EventContext = createContext({
3442
3910
  eventBus: null
@@ -3448,20 +3916,20 @@ const LayoutContext = createContext({
3448
3916
  setLayoutForKey: () => {}
3449
3917
  });
3450
3918
 
3451
- /**
3452
- * Accesses the global DescriptionContext and returns a description for a given id and element.
3453
- *
3454
- * @example
3455
- * ```jsx
3456
- * function TextField(props) {
3457
- * const description = useDescriptionContext('input1', element);
3458
- * }
3459
- * ```
3460
- *
3461
- * @param {string} id
3462
- * @param {object} element
3463
- *
3464
- * @returns {string}
3919
+ /**
3920
+ * Accesses the global DescriptionContext and returns a description for a given id and element.
3921
+ *
3922
+ * @example
3923
+ * ```jsx
3924
+ * function TextField(props) {
3925
+ * const description = useDescriptionContext('input1', element);
3926
+ * }
3927
+ * ```
3928
+ *
3929
+ * @param {string} id
3930
+ * @param {object} element
3931
+ *
3932
+ * @returns {string}
3465
3933
  */
3466
3934
  function useDescriptionContext(id, element) {
3467
3935
  const {
@@ -3476,11 +3944,11 @@ function useError(id) {
3476
3944
  return errors[id];
3477
3945
  }
3478
3946
 
3479
- /**
3480
- * Subscribe to an event immediately. Update subscription after inputs changed.
3481
- *
3482
- * @param {string} event
3483
- * @param {Function} callback
3947
+ /**
3948
+ * Subscribe to an event immediately. Update subscription after inputs changed.
3949
+ *
3950
+ * @param {string} event
3951
+ * @param {Function} callback
3484
3952
  */
3485
3953
  function useEvent(event, callback, eventBus) {
3486
3954
  const eventContext = useContext(EventContext);
@@ -3510,20 +3978,20 @@ function useEvent(event, callback, eventBus) {
3510
3978
  }, [callback, event, eventBus]);
3511
3979
  }
3512
3980
 
3513
- /**
3514
- * Creates a state that persists in the global LayoutContext.
3515
- *
3516
- * @example
3517
- * ```jsx
3518
- * function Group(props) {
3519
- * const [ open, setOpen ] = useLayoutState([ 'groups', 'foo', 'open' ], false);
3520
- * }
3521
- * ```
3522
- *
3523
- * @param {(string|number)[]} path
3524
- * @param {any} [defaultValue]
3525
- *
3526
- * @returns {[ any, Function ]}
3981
+ /**
3982
+ * Creates a state that persists in the global LayoutContext.
3983
+ *
3984
+ * @example
3985
+ * ```jsx
3986
+ * function Group(props) {
3987
+ * const [ open, setOpen ] = useLayoutState([ 'groups', 'foo', 'open' ], false);
3988
+ * }
3989
+ * ```
3990
+ *
3991
+ * @param {(string|number)[]} path
3992
+ * @param {any} [defaultValue]
3993
+ *
3994
+ * @returns {[ any, Function ]}
3527
3995
  */
3528
3996
  function useLayoutState(path, defaultValue) {
3529
3997
  const {
@@ -3531,22 +3999,17 @@ function useLayoutState(path, defaultValue) {
3531
3999
  setLayoutForKey
3532
4000
  } = useContext(LayoutContext);
3533
4001
  const layoutForKey = getLayoutForKey(path, defaultValue);
3534
- const [value, set] = useState(layoutForKey);
3535
- const setState = newValue => {
3536
- // (1) set component state
3537
- set(newValue);
3538
-
3539
- // (2) set context
4002
+ const setState = useCallback(newValue => {
3540
4003
  setLayoutForKey(path, newValue);
3541
- };
3542
- return [value, setState];
4004
+ }, [setLayoutForKey]);
4005
+ return [layoutForKey, setState];
3543
4006
  }
3544
4007
 
3545
- /**
3546
- * @pinussilvestrus: we need to introduce our own hook to persist the previous
3547
- * state on updates.
3548
- *
3549
- * cf. https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
4008
+ /**
4009
+ * @pinussilvestrus: we need to introduce our own hook to persist the previous
4010
+ * state on updates.
4011
+ *
4012
+ * cf. https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
3550
4013
  */
3551
4014
 
3552
4015
  function usePrevious(value) {
@@ -3557,12 +4020,12 @@ function usePrevious(value) {
3557
4020
  return ref.current;
3558
4021
  }
3559
4022
 
3560
- /**
3561
- * Subscribe to `propertiesPanel.showEntry`.
3562
- *
3563
- * @param {string} id
3564
- *
3565
- * @returns {import('preact').Ref}
4023
+ /**
4024
+ * Subscribe to `propertiesPanel.showEntry`.
4025
+ *
4026
+ * @param {string} id
4027
+ *
4028
+ * @returns {import('preact').Ref}
3566
4029
  */
3567
4030
  function useShowEntryEvent(id) {
3568
4031
  const {
@@ -3593,20 +4056,20 @@ function useShowEntryEvent(id) {
3593
4056
  return ref;
3594
4057
  }
3595
4058
 
3596
- /**
3597
- * @callback setSticky
3598
- * @param {boolean} value
4059
+ /**
4060
+ * @callback setSticky
4061
+ * @param {boolean} value
3599
4062
  */
3600
4063
 
3601
- /**
3602
- * Use IntersectionObserver to identify when DOM element is in sticky mode.
3603
- * If sticky is observered setSticky(true) will be called.
3604
- * If sticky mode is left, setSticky(false) will be called.
3605
- *
3606
- *
3607
- * @param {Object} ref
3608
- * @param {string} scrollContainerSelector
3609
- * @param {setSticky} setSticky
4064
+ /**
4065
+ * Use IntersectionObserver to identify when DOM element is in sticky mode.
4066
+ * If sticky is observered setSticky(true) will be called.
4067
+ * If sticky mode is left, setSticky(false) will be called.
4068
+ *
4069
+ *
4070
+ * @param {Object} ref
4071
+ * @param {string} scrollContainerSelector
4072
+ * @param {setSticky} setSticky
3610
4073
  */
3611
4074
  function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky) {
3612
4075
  useEffect(() => {
@@ -3645,19 +4108,19 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
3645
4108
  }, [ref, scrollContainerSelector, setSticky]);
3646
4109
  }
3647
4110
 
3648
- /**
3649
- * Creates a static function reference with changing body.
3650
- * This is necessary when external libraries require a callback function
3651
- * that has references to state variables.
3652
- *
3653
- * Usage:
3654
- * const callback = useStaticCallback((val) => {val === currentState});
3655
- *
3656
- * The `callback` reference is static and can be safely used in external
3657
- * libraries or as a prop that does not cause rerendering of children.
3658
- *
3659
- * @param {Function} callback function with changing reference
3660
- * @returns {Function} static function reference
4111
+ /**
4112
+ * Creates a static function reference with changing body.
4113
+ * This is necessary when external libraries require a callback function
4114
+ * that has references to state variables.
4115
+ *
4116
+ * Usage:
4117
+ * const callback = useStaticCallback((val) => {val === currentState});
4118
+ *
4119
+ * The `callback` reference is static and can be safely used in external
4120
+ * libraries or as a prop that does not cause rerendering of children.
4121
+ *
4122
+ * @param {Function} callback function with changing reference
4123
+ * @returns {Function} static function reference
3661
4124
  */
3662
4125
  function useStaticCallback(callback) {
3663
4126
  const callbackRef = useRef(callback);
@@ -3749,13 +4212,13 @@ function DataMarker() {
3749
4212
  });
3750
4213
  }
3751
4214
 
3752
- /**
3753
- * @typedef { {
3754
- * text: (element: object) => string,
3755
- * icon?: (element: Object) => import('preact').Component
3756
- * } } PlaceholderDefinition
3757
- *
3758
- * @param { PlaceholderDefinition } props
4215
+ /**
4216
+ * @typedef { {
4217
+ * text: (element: object) => string,
4218
+ * icon?: (element: Object) => import('preact').Component
4219
+ * } } PlaceholderDefinition
4220
+ *
4221
+ * @param { PlaceholderDefinition } props
3759
4222
  */
3760
4223
  function Placeholder(props) {
3761
4224
  const {
@@ -3780,72 +4243,72 @@ const DEFAULT_LAYOUT = {
3780
4243
  };
3781
4244
  const DEFAULT_DESCRIPTION = {};
3782
4245
 
3783
- /**
3784
- * @typedef { {
3785
- * component: import('preact').Component,
3786
- * id: String,
3787
- * isEdited?: Function
3788
- * } } EntryDefinition
3789
- *
3790
- * @typedef { {
3791
- * autoFocusEntry: String,
3792
- * autoOpen?: Boolean,
3793
- * entries: Array<EntryDefinition>,
3794
- * id: String,
3795
- * label: String,
3796
- * remove: (event: MouseEvent) => void
3797
- * } } ListItemDefinition
3798
- *
3799
- * @typedef { {
3800
- * add: (event: MouseEvent) => void,
3801
- * component: import('preact').Component,
3802
- * element: Object,
3803
- * id: String,
3804
- * items: Array<ListItemDefinition>,
3805
- * label: String,
3806
- * shouldSort?: Boolean,
3807
- * shouldOpen?: Boolean
3808
- * } } ListGroupDefinition
3809
- *
3810
- * @typedef { {
3811
- * component?: import('preact').Component,
3812
- * entries: Array<EntryDefinition>,
3813
- * id: String,
3814
- * label: String,
3815
- * shouldOpen?: Boolean
3816
- * } } GroupDefinition
3817
- *
3818
- * @typedef { {
3819
- * [id: String]: GetDescriptionFunction
3820
- * } } DescriptionConfig
3821
- *
3822
- * @callback { {
3823
- * @param {string} id
3824
- * @param {Object} element
3825
- * @returns {string}
3826
- * } } GetDescriptionFunction
3827
- *
3828
- * @typedef { {
3829
- * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
3830
- * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
3831
- * } } PlaceholderProvider
3832
- *
4246
+ /**
4247
+ * @typedef { {
4248
+ * component: import('preact').Component,
4249
+ * id: String,
4250
+ * isEdited?: Function
4251
+ * } } EntryDefinition
4252
+ *
4253
+ * @typedef { {
4254
+ * autoFocusEntry: String,
4255
+ * autoOpen?: Boolean,
4256
+ * entries: Array<EntryDefinition>,
4257
+ * id: String,
4258
+ * label: String,
4259
+ * remove: (event: MouseEvent) => void
4260
+ * } } ListItemDefinition
4261
+ *
4262
+ * @typedef { {
4263
+ * add: (event: MouseEvent) => void,
4264
+ * component: import('preact').Component,
4265
+ * element: Object,
4266
+ * id: String,
4267
+ * items: Array<ListItemDefinition>,
4268
+ * label: String,
4269
+ * shouldSort?: Boolean,
4270
+ * shouldOpen?: Boolean
4271
+ * } } ListGroupDefinition
4272
+ *
4273
+ * @typedef { {
4274
+ * component?: import('preact').Component,
4275
+ * entries: Array<EntryDefinition>,
4276
+ * id: String,
4277
+ * label: String,
4278
+ * shouldOpen?: Boolean
4279
+ * } } GroupDefinition
4280
+ *
4281
+ * @typedef { {
4282
+ * [id: String]: GetDescriptionFunction
4283
+ * } } DescriptionConfig
4284
+ *
4285
+ * @callback { {
4286
+ * @param {string} id
4287
+ * @param {Object} element
4288
+ * @returns {string}
4289
+ * } } GetDescriptionFunction
4290
+ *
4291
+ * @typedef { {
4292
+ * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
4293
+ * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
4294
+ * } } PlaceholderProvider
4295
+ *
3833
4296
  */
3834
4297
 
3835
- /**
3836
- * A basic properties panel component. Describes *how* content will be rendered, accepts
3837
- * data from implementor to describe *what* will be rendered.
3838
- *
3839
- * @param {Object} props
3840
- * @param {Object|Array} props.element
3841
- * @param {import('./components/Header').HeaderProvider} props.headerProvider
3842
- * @param {PlaceholderProvider} [props.placeholderProvider]
3843
- * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
3844
- * @param {Object} [props.layoutConfig]
3845
- * @param {Function} [props.layoutChanged]
3846
- * @param {DescriptionConfig} [props.descriptionConfig]
3847
- * @param {Function} [props.descriptionLoaded]
3848
- * @param {Object} [props.eventBus]
4298
+ /**
4299
+ * A basic properties panel component. Describes *how* content will be rendered, accepts
4300
+ * data from implementor to describe *what* will be rendered.
4301
+ *
4302
+ * @param {Object} props
4303
+ * @param {Object|Array} props.element
4304
+ * @param {import('./components/Header').HeaderProvider} props.headerProvider
4305
+ * @param {PlaceholderProvider} [props.placeholderProvider]
4306
+ * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
4307
+ * @param {Object} [props.layoutConfig]
4308
+ * @param {Function} [props.layoutChanged]
4309
+ * @param {DescriptionConfig} [props.descriptionConfig]
4310
+ * @param {Function} [props.descriptionLoaded]
4311
+ * @param {Object} [props.eventBus]
3849
4312
  */
3850
4313
  function PropertiesPanel(props) {
3851
4314
  const {
@@ -3853,15 +4316,21 @@ function PropertiesPanel(props) {
3853
4316
  headerProvider,
3854
4317
  placeholderProvider,
3855
4318
  groups,
3856
- layoutConfig = {},
4319
+ layoutConfig,
3857
4320
  layoutChanged,
3858
- descriptionConfig = {},
4321
+ descriptionConfig,
3859
4322
  descriptionLoaded,
3860
4323
  eventBus
3861
4324
  } = props;
3862
4325
 
3863
4326
  // set-up layout context
3864
4327
  const [layout, setLayout] = useState(createLayout(layoutConfig));
4328
+
4329
+ // react to external changes in the layout config
4330
+ useUpdateEffect(() => {
4331
+ const newLayout = createLayout(layoutConfig);
4332
+ setLayout(newLayout);
4333
+ }, [layoutConfig]);
3865
4334
  useEffect(() => {
3866
4335
  if (typeof layoutChanged === 'function') {
3867
4336
  layoutChanged(layout);
@@ -3883,10 +4352,12 @@ function PropertiesPanel(props) {
3883
4352
  };
3884
4353
 
3885
4354
  // set-up description context
3886
- const description = createDescriptionContext(descriptionConfig);
3887
- if (typeof descriptionLoaded === 'function') {
3888
- descriptionLoaded(description);
3889
- }
4355
+ const description = useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
4356
+ useEffect(() => {
4357
+ if (typeof descriptionLoaded === 'function') {
4358
+ descriptionLoaded(description);
4359
+ }
4360
+ }, [description, descriptionLoaded]);
3890
4361
  const getDescriptionForId = (id, element) => {
3891
4362
  return description[id] && description[id](element);
3892
4363
  };
@@ -3961,18 +4432,37 @@ function PropertiesPanel(props) {
3961
4432
 
3962
4433
  // helpers //////////////////
3963
4434
 
3964
- function createLayout(overrides) {
4435
+ function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
3965
4436
  return {
3966
- ...DEFAULT_LAYOUT,
4437
+ ...defaults,
3967
4438
  ...overrides
3968
4439
  };
3969
4440
  }
3970
- function createDescriptionContext(overrides) {
4441
+ function createDescriptionContext(overrides = {}) {
3971
4442
  return {
3972
4443
  ...DEFAULT_DESCRIPTION,
3973
4444
  ...overrides
3974
4445
  };
3975
4446
  }
4447
+
4448
+ // hooks //////////////////
4449
+
4450
+ /**
4451
+ * This hook behaves like useEffect, but does not trigger on the first render.
4452
+ *
4453
+ * @param {Function} effect
4454
+ * @param {Array} deps
4455
+ */
4456
+ function useUpdateEffect(effect, deps) {
4457
+ const isMounted = useRef(false);
4458
+ useEffect(() => {
4459
+ if (isMounted.current) {
4460
+ return effect();
4461
+ } else {
4462
+ isMounted.current = true;
4463
+ }
4464
+ }, deps);
4465
+ }
3976
4466
  function CollapsibleEntry(props) {
3977
4467
  const {
3978
4468
  element,
@@ -4068,10 +4558,10 @@ function ListItem(props) {
4068
4558
  })
4069
4559
  });
4070
4560
  }
4071
- const noop$2 = () => {};
4561
+ const noop$3 = () => {};
4072
4562
 
4073
- /**
4074
- * @param {import('../PropertiesPanel').ListGroupDefinition} props
4563
+ /**
4564
+ * @param {import('../PropertiesPanel').ListGroupDefinition} props
4075
4565
  */
4076
4566
  function ListGroup(props) {
4077
4567
  const {
@@ -4089,6 +4579,9 @@ function ListGroup(props) {
4089
4579
  const onShow = useCallback(() => setOpen(true), [setOpen]);
4090
4580
  const [ordering, setOrdering] = useState([]);
4091
4581
  const [newItemAdded, setNewItemAdded] = useState(false);
4582
+
4583
+ // Flag to mark that add button was clicked in the last render cycle
4584
+ const [addTriggered, setAddTriggered] = useState(false);
4092
4585
  const prevItems = usePrevious(items);
4093
4586
  const prevElement = usePrevious(element);
4094
4587
  const elementChanged = element !== prevElement;
@@ -4110,6 +4603,8 @@ function ListGroup(props) {
4110
4603
 
4111
4604
  // (1) items were added
4112
4605
  useEffect(() => {
4606
+ // reset addTriggered flag
4607
+ setAddTriggered(false);
4113
4608
  if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
4114
4609
  let add = [];
4115
4610
  items.forEach(item => {
@@ -4119,14 +4614,18 @@ function ListGroup(props) {
4119
4614
  });
4120
4615
  let newOrdering = ordering;
4121
4616
 
4122
- // open if not open and configured
4123
- if (!open && shouldOpen) {
4617
+ // open if not open, configured and triggered by add button
4618
+ //
4619
+ // TODO(marstamm): remove once we refactor layout handling for listGroups.
4620
+ // Ideally, opening should be handled as part of the `add` callback and
4621
+ // not be a concern for the ListGroup component.
4622
+ if (addTriggered && !open && shouldOpen) {
4124
4623
  toggleOpen();
4624
+ }
4125
4625
 
4126
- // if I opened and I should sort, then sort items
4127
- if (shouldSort) {
4128
- newOrdering = createOrdering(sortItems(items));
4129
- }
4626
+ // filter when not open and configured
4627
+ if (!open && shouldSort) {
4628
+ newOrdering = createOrdering(sortItems(items));
4130
4629
  }
4131
4630
 
4132
4631
  // add new items on top or bottom depending on sorting behavior
@@ -4137,11 +4636,11 @@ function ListGroup(props) {
4137
4636
  newOrdering.push(...add);
4138
4637
  }
4139
4638
  setOrdering(newOrdering);
4140
- setNewItemAdded(true);
4639
+ setNewItemAdded(addTriggered);
4141
4640
  } else {
4142
4641
  setNewItemAdded(false);
4143
4642
  }
4144
- }, [items, open, shouldHandleEffects]);
4643
+ }, [items, open, shouldHandleEffects, addTriggered]);
4145
4644
 
4146
4645
  // (2) sort items on open if shouldSort is set
4147
4646
  useEffect(() => {
@@ -4171,13 +4670,17 @@ function ListGroup(props) {
4171
4670
  ...useContext(LayoutContext),
4172
4671
  onShow
4173
4672
  };
4673
+ const handleAddClick = e => {
4674
+ setAddTriggered(true);
4675
+ add(e);
4676
+ };
4174
4677
  return jsxs("div", {
4175
4678
  class: "bio-properties-panel-group",
4176
4679
  "data-group-id": 'group-' + id,
4177
4680
  ref: groupRef,
4178
4681
  children: [jsxs("div", {
4179
4682
  class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
4180
- onClick: hasItems ? toggleOpen : noop$2,
4683
+ onClick: hasItems ? toggleOpen : noop$3,
4181
4684
  children: [jsx("div", {
4182
4685
  title: label,
4183
4686
  class: "bio-properties-panel-group-header-title",
@@ -4187,7 +4690,7 @@ function ListGroup(props) {
4187
4690
  children: [add ? jsxs("button", {
4188
4691
  title: "Create new list item",
4189
4692
  class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
4190
- onClick: add,
4693
+ onClick: handleAddClick,
4191
4694
  children: [jsx(CreateIcon, {}), !hasItems ? jsx("span", {
4192
4695
  class: "bio-properties-panel-add-entry-label",
4193
4696
  children: "Create"
@@ -4217,8 +4720,9 @@ function ListGroup(props) {
4217
4720
  id
4218
4721
  } = item;
4219
4722
 
4220
- // if item was added, open first or last item based on ordering
4221
- const autoOpen = newItemAdded && (shouldSort ? index === 0 : index === ordering.length - 1);
4723
+ // if item was added, open it
4724
+ // Existing items will not be affected as autoOpen is only applied on first render
4725
+ const autoOpen = newItemAdded;
4222
4726
  return createElement(ListItem, {
4223
4727
  ...item,
4224
4728
  autoOpen: autoOpen,
@@ -4234,8 +4738,8 @@ function ListGroup(props) {
4234
4738
 
4235
4739
  // helpers ////////////////////
4236
4740
 
4237
- /**
4238
- * Sorts given items alphanumeric by label
4741
+ /**
4742
+ * Sorts given items alphanumeric by label
4239
4743
  */
4240
4744
  function sortItems(items) {
4241
4745
  return sortBy(items, i => i.label.toLowerCase());
@@ -4309,17 +4813,17 @@ function Checkbox(props) {
4309
4813
  });
4310
4814
  }
4311
4815
 
4312
- /**
4313
- * @param {Object} props
4314
- * @param {Object} props.element
4315
- * @param {String} props.id
4316
- * @param {String} props.description
4317
- * @param {String} props.label
4318
- * @param {Function} props.getValue
4319
- * @param {Function} props.setValue
4320
- * @param {Function} props.onFocus
4321
- * @param {Function} props.onBlur
4322
- * @param {boolean} [props.disabled]
4816
+ /**
4817
+ * @param {Object} props
4818
+ * @param {Object} props.element
4819
+ * @param {String} props.id
4820
+ * @param {String} props.description
4821
+ * @param {String} props.label
4822
+ * @param {Function} props.getValue
4823
+ * @param {Function} props.setValue
4824
+ * @param {Function} props.onFocus
4825
+ * @param {Function} props.onBlur
4826
+ * @param {boolean} [props.disabled]
4323
4827
  */
4324
4828
  function CheckboxEntry(props) {
4325
4829
  const {
@@ -4356,7 +4860,7 @@ function CheckboxEntry(props) {
4356
4860
  })]
4357
4861
  });
4358
4862
  }
4359
- function isEdited$7(node) {
4863
+ function isEdited$8(node) {
4360
4864
  return node && !!node.checked;
4361
4865
  }
4362
4866
 
@@ -4365,6 +4869,87 @@ function isEdited$7(node) {
4365
4869
  function prefixId$7(id) {
4366
4870
  return `bio-properties-panel-${id}`;
4367
4871
  }
4872
+ const useBufferedFocus$1 = function (editor, ref) {
4873
+ const [buffer, setBuffer] = useState(undefined);
4874
+ ref.current = useMemo(() => ({
4875
+ focus: offset => {
4876
+ if (editor) {
4877
+ editor.focus(offset);
4878
+ } else {
4879
+ if (typeof offset === 'undefined') {
4880
+ offset = Infinity;
4881
+ }
4882
+ setBuffer(offset);
4883
+ }
4884
+ }
4885
+ }), [editor]);
4886
+ useEffect(() => {
4887
+ if (typeof buffer !== 'undefined' && editor) {
4888
+ editor.focus(buffer);
4889
+ setBuffer(false);
4890
+ }
4891
+ }, [editor, buffer]);
4892
+ };
4893
+ const CodeEditor$1 = forwardRef((props, ref) => {
4894
+ const {
4895
+ onInput,
4896
+ disabled,
4897
+ tooltipContainer,
4898
+ enableGutters,
4899
+ value,
4900
+ onLint = () => {},
4901
+ contentAttributes = {},
4902
+ hostLanguage = null,
4903
+ singleLine = false
4904
+ } = props;
4905
+ const inputRef = useRef();
4906
+ const [editor, setEditor] = useState();
4907
+ const [localValue, setLocalValue] = useState(value || '');
4908
+ useBufferedFocus$1(editor, ref);
4909
+ const handleInput = useStaticCallback(newValue => {
4910
+ onInput(newValue);
4911
+ setLocalValue(newValue);
4912
+ });
4913
+ useEffect(() => {
4914
+ let editor;
4915
+ editor = new FeelersEditor({
4916
+ container: inputRef.current,
4917
+ onChange: handleInput,
4918
+ value: localValue,
4919
+ onLint,
4920
+ contentAttributes,
4921
+ tooltipContainer,
4922
+ enableGutters,
4923
+ hostLanguage,
4924
+ singleLine
4925
+ });
4926
+ setEditor(editor);
4927
+ return () => {
4928
+ onLint([]);
4929
+ inputRef.current.innerHTML = '';
4930
+ setEditor(null);
4931
+ };
4932
+ }, []);
4933
+ useEffect(() => {
4934
+ if (!editor) {
4935
+ return;
4936
+ }
4937
+ if (value === localValue) {
4938
+ return;
4939
+ }
4940
+ editor.setValue(value);
4941
+ setLocalValue(value);
4942
+ }, [value]);
4943
+ const handleClick = () => {
4944
+ ref.current.focus();
4945
+ };
4946
+ return jsx("div", {
4947
+ name: props.name,
4948
+ class: classnames('bio-properties-panel-feelers-editor bio-properties-panel-input', localValue ? 'edited' : null, disabled ? 'disabled' : null),
4949
+ ref: inputRef,
4950
+ onClick: handleClick
4951
+ });
4952
+ });
4368
4953
  const useBufferedFocus = function (editor, ref) {
4369
4954
  const [buffer, setBuffer] = useState(undefined);
4370
4955
  ref.current = useMemo(() => ({
@@ -4407,10 +4992,10 @@ const CodeEditor = forwardRef((props, ref) => {
4407
4992
  useEffect(() => {
4408
4993
  let editor;
4409
4994
 
4410
- /* Trigger FEEL toggle when
4411
- *
4412
- * - `backspace` is pressed
4413
- * - AND the cursor is at the beginning of the input
4995
+ /* Trigger FEEL toggle when
4996
+ *
4997
+ * - `backspace` is pressed
4998
+ * - AND the cursor is at the beginning of the input
4414
4999
  */
4415
5000
  const onKeyDown = e => {
4416
5001
  if (e.key !== 'Backspace' || !editor) {
@@ -4479,12 +5064,12 @@ function FeelIndicator(props) {
4479
5064
  children: "="
4480
5065
  });
4481
5066
  }
4482
- const noop$1 = () => {};
5067
+ const noop$2 = () => {};
4483
5068
 
4484
- /**
4485
- * @param {Object} props
4486
- * @param {Object} props.label
4487
- * @param {String} props.feel
5069
+ /**
5070
+ * @param {Object} props
5071
+ * @param {Object} props.label
5072
+ * @param {String} props.feel
4488
5073
  */
4489
5074
  function FeelIcon(props) {
4490
5075
  const {
@@ -4492,7 +5077,7 @@ function FeelIcon(props) {
4492
5077
  feel = false,
4493
5078
  active,
4494
5079
  disabled = false,
4495
- onClick = noop$1
5080
+ onClick = noop$2
4496
5081
  } = props;
4497
5082
  const feelRequiredLabel = ' must be a FEEL expression';
4498
5083
  const feelOptionalLabel = ' can optionally be a FEEL expression';
@@ -4512,7 +5097,7 @@ function FeelIcon(props) {
4512
5097
  children: feel === 'required' ? jsx(FeelRequiredIcon, {}) : jsx(FeelOptionalIcon, {})
4513
5098
  });
4514
5099
  }
4515
- const noop = () => {};
5100
+ const noop$1 = () => {};
4516
5101
  function FeelTextfield(props) {
4517
5102
  const {
4518
5103
  debounce,
@@ -4669,6 +5254,9 @@ function FeelTextfield(props) {
4669
5254
  }) : jsx(OptionalComponent, {
4670
5255
  ...props,
4671
5256
  onInput: handleLocalInput,
5257
+ contentAttributes: {
5258
+ 'id': prefixId$6(id)
5259
+ },
4672
5260
  value: localValue,
4673
5261
  ref: editorRef
4674
5262
  })]
@@ -4718,7 +5306,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
4718
5306
  value: value || ''
4719
5307
  });
4720
5308
  });
4721
- const OptionalFeelTextArea = forwardRef((props, ref) => {
5309
+ forwardRef((props, ref) => {
4722
5310
  const {
4723
5311
  id,
4724
5312
  disabled,
@@ -4758,17 +5346,24 @@ const OptionalFeelTextArea = forwardRef((props, ref) => {
4758
5346
  });
4759
5347
  });
4760
5348
 
4761
- /**
4762
- * @param {Object} props
4763
- * @param {Object} props.element
4764
- * @param {String} props.id
4765
- * @param {String} props.description
4766
- * @param {Boolean} props.debounce
4767
- * @param {Boolean} props.disabled
4768
- * @param {String} props.label
4769
- * @param {Function} props.getValue
4770
- * @param {Function} props.setValue
4771
- * @param {Function} props.validate
5349
+ /**
5350
+ * @param {Object} props
5351
+ * @param {Object} props.element
5352
+ * @param {String} props.id
5353
+ * @param {String} props.description
5354
+ * @param {Boolean} props.debounce
5355
+ * @param {Boolean} props.disabled
5356
+ * @param {Boolean} props.feel
5357
+ * @param {String} props.label
5358
+ * @param {Function} props.getValue
5359
+ * @param {Function} props.setValue
5360
+ * @param {Function} props.tooltipContainer
5361
+ * @param {Function} props.validate
5362
+ * @param {Function} props.show
5363
+ * @param {Function} props.example
5364
+ * @param {Function} props.variables
5365
+ * @param {Function} props.onFocus
5366
+ * @param {Function} props.onBlur
4772
5367
  */
4773
5368
  function FeelEntry(props) {
4774
5369
  const {
@@ -4782,8 +5377,10 @@ function FeelEntry(props) {
4782
5377
  getValue,
4783
5378
  setValue,
4784
5379
  tooltipContainer,
5380
+ hostLanguage,
5381
+ singleLine,
4785
5382
  validate,
4786
- show = noop,
5383
+ show = noop$1,
4787
5384
  example,
4788
5385
  variables,
4789
5386
  onFocus,
@@ -4837,6 +5434,8 @@ function FeelEntry(props) {
4837
5434
  onFocus: onFocus,
4838
5435
  onBlur: onBlur,
4839
5436
  example: example,
5437
+ hostLanguage: hostLanguage,
5438
+ singleLine: singleLine,
4840
5439
  show: show,
4841
5440
  value: value,
4842
5441
  variables: variables,
@@ -4853,28 +5452,35 @@ function FeelEntry(props) {
4853
5452
  });
4854
5453
  }
4855
5454
 
4856
- /**
4857
- * @param {Object} props
4858
- * @param {Object} props.element
4859
- * @param {String} props.id
4860
- * @param {String} props.description
4861
- * @param {Boolean} props.debounce
4862
- * @param {Boolean} props.disabled
4863
- * @param {String} props.label
4864
- * @param {Function} props.getValue
4865
- * @param {Function} props.setValue
4866
- * @param {Function} props.onFocus
4867
- * @param {Function} props.onBlur
4868
- * @param {Function} props.validate
5455
+ /**
5456
+ * @param {Object} props
5457
+ * @param {Object} props.element
5458
+ * @param {String} props.id
5459
+ * @param {String} props.description
5460
+ * @param {String} props.hostLanguage
5461
+ * @param {Boolean} props.singleLine
5462
+ * @param {Boolean} props.debounce
5463
+ * @param {Boolean} props.disabled
5464
+ * @param {Boolean} props.feel
5465
+ * @param {String} props.label
5466
+ * @param {Function} props.getValue
5467
+ * @param {Function} props.setValue
5468
+ * @param {Function} props.tooltipContainer
5469
+ * @param {Function} props.validate
5470
+ * @param {Function} props.show
5471
+ * @param {Function} props.example
5472
+ * @param {Function} props.variables
5473
+ * @param {Function} props.onFocus
5474
+ * @param {Function} props.onBlur
4869
5475
  */
4870
- function FeelTextArea(props) {
5476
+ function FeelTemplatingEntry(props) {
4871
5477
  return jsx(FeelEntry, {
4872
- class: "bio-properties-panel-feel-textarea",
4873
- OptionalComponent: OptionalFeelTextArea,
5478
+ class: "bio-properties-panel-feel-templating",
5479
+ OptionalComponent: CodeEditor$1,
4874
5480
  ...props
4875
5481
  });
4876
5482
  }
4877
- function isEdited$6(node) {
5483
+ function isEdited$7(node) {
4878
5484
  return node && (!!node.value || node.classList.contains('edited'));
4879
5485
  }
4880
5486
 
@@ -4944,22 +5550,22 @@ function NumberField(props) {
4944
5550
  });
4945
5551
  }
4946
5552
 
4947
- /**
4948
- * @param {Object} props
4949
- * @param {Boolean} props.debounce
4950
- * @param {String} props.description
4951
- * @param {Boolean} props.disabled
4952
- * @param {Object} props.element
4953
- * @param {Function} props.getValue
4954
- * @param {String} props.id
4955
- * @param {String} props.label
4956
- * @param {String} props.max
4957
- * @param {String} props.min
4958
- * @param {Function} props.setValue
4959
- * @param {Function} props.onFocus
4960
- * @param {Function} props.onBlur
4961
- * @param {String} props.step
4962
- * @param {Function} props.validate
5553
+ /**
5554
+ * @param {Object} props
5555
+ * @param {Boolean} props.debounce
5556
+ * @param {String} props.description
5557
+ * @param {Boolean} props.disabled
5558
+ * @param {Object} props.element
5559
+ * @param {Function} props.getValue
5560
+ * @param {String} props.id
5561
+ * @param {String} props.label
5562
+ * @param {String} props.max
5563
+ * @param {String} props.min
5564
+ * @param {Function} props.setValue
5565
+ * @param {Function} props.onFocus
5566
+ * @param {Function} props.onBlur
5567
+ * @param {String} props.step
5568
+ * @param {Function} props.validate
4963
5569
  */
4964
5570
  function NumberFieldEntry(props) {
4965
5571
  const {
@@ -5084,6 +5690,16 @@ function Select(props) {
5084
5690
  value: localValue,
5085
5691
  disabled: disabled,
5086
5692
  children: options.map((option, idx) => {
5693
+ if (option.children) {
5694
+ return jsx("optgroup", {
5695
+ label: option.label,
5696
+ children: option.children.map((child, idx) => jsx("option", {
5697
+ value: child.value,
5698
+ disabled: child.disabled,
5699
+ children: child.label
5700
+ }, idx))
5701
+ }, idx);
5702
+ }
5087
5703
  return jsx("option", {
5088
5704
  value: option.value,
5089
5705
  disabled: option.disabled,
@@ -5094,18 +5710,19 @@ function Select(props) {
5094
5710
  });
5095
5711
  }
5096
5712
 
5097
- /**
5098
- * @param {object} props
5099
- * @param {object} props.element
5100
- * @param {string} props.id
5101
- * @param {string} [props.description]
5102
- * @param {string} props.label
5103
- * @param {Function} props.getValue
5104
- * @param {Function} props.setValue
5105
- * @param {Function} props.onFocus
5106
- * @param {Function} props.onBlur
5107
- * @param {Function} props.getOptions
5108
- * @param {boolean} [props.disabled]
5713
+ /**
5714
+ * @param {object} props
5715
+ * @param {object} props.element
5716
+ * @param {string} props.id
5717
+ * @param {string} [props.description]
5718
+ * @param {string} props.label
5719
+ * @param {Function} props.getValue
5720
+ * @param {Function} props.setValue
5721
+ * @param {Function} props.onFocus
5722
+ * @param {Function} props.onBlur
5723
+ * @param {Function} props.getOptions
5724
+ * @param {boolean} [props.disabled]
5725
+ * @param {Function} [props.validate]
5109
5726
  */
5110
5727
  function SelectEntry(props) {
5111
5728
  const {
@@ -5118,11 +5735,37 @@ function SelectEntry(props) {
5118
5735
  getOptions,
5119
5736
  disabled,
5120
5737
  onFocus,
5121
- onBlur
5738
+ onBlur,
5739
+ validate
5122
5740
  } = props;
5123
- const value = getValue(element);
5124
5741
  const options = getOptions(element);
5125
- const error = useError(id);
5742
+ const [cachedInvalidValue, setCachedInvalidValue] = useState(null);
5743
+ const globalError = useError(id);
5744
+ const [localError, setLocalError] = useState(null);
5745
+ let value = getValue(element);
5746
+ const previousValue = usePrevious(value);
5747
+ useEffect(() => {
5748
+ if (isFunction(validate)) {
5749
+ const newValidationError = validate(value) || null;
5750
+ setLocalError(newValidationError);
5751
+ }
5752
+ }, [value]);
5753
+ const onChange = newValue => {
5754
+ let newValidationError = null;
5755
+ if (isFunction(validate)) {
5756
+ newValidationError = validate(newValue) || null;
5757
+ }
5758
+ if (newValidationError) {
5759
+ setCachedInvalidValue(newValue);
5760
+ } else {
5761
+ setValue(newValue);
5762
+ }
5763
+ setLocalError(newValidationError);
5764
+ };
5765
+ if (previousValue === value && localError) {
5766
+ value = cachedInvalidValue;
5767
+ }
5768
+ const error = globalError || localError;
5126
5769
  return jsxs("div", {
5127
5770
  class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
5128
5771
  "data-entry-id": id,
@@ -5130,7 +5773,7 @@ function SelectEntry(props) {
5130
5773
  id: id,
5131
5774
  label: label,
5132
5775
  value: value,
5133
- onChange: setValue,
5776
+ onChange: onChange,
5134
5777
  onFocus: onFocus,
5135
5778
  onBlur: onBlur,
5136
5779
  options: options,
@@ -5154,18 +5797,26 @@ function isEdited$4(node) {
5154
5797
  function prefixId$4(id) {
5155
5798
  return `bio-properties-panel-${id}`;
5156
5799
  }
5800
+ function resizeToContents(element) {
5801
+ element.style.height = 'auto';
5802
+
5803
+ // a 2px pixel offset is required to prevent scrollbar from
5804
+ // appearing on OS with a full length scroll bar (Windows/Linux)
5805
+ element.style.height = `${element.scrollHeight + 2}px`;
5806
+ }
5157
5807
  function TextArea(props) {
5158
5808
  const {
5159
5809
  id,
5160
5810
  label,
5161
- rows = 2,
5162
5811
  debounce,
5163
5812
  onInput,
5164
5813
  value = '',
5165
5814
  disabled,
5166
5815
  monospace,
5167
5816
  onFocus,
5168
- onBlur
5817
+ onBlur,
5818
+ autoResize,
5819
+ rows = autoResize ? 1 : 2
5169
5820
  } = props;
5170
5821
  const [localValue, setLocalValue] = useState(value);
5171
5822
  const ref = useShowEntryEvent(id);
@@ -5176,8 +5827,12 @@ function TextArea(props) {
5176
5827
  }, [onInput, debounce]);
5177
5828
  const handleInput = e => {
5178
5829
  handleInputCallback(e);
5830
+ autoResize && resizeToContents(e.target);
5179
5831
  setLocalValue(e.target.value);
5180
5832
  };
5833
+ useLayoutEffect(() => {
5834
+ autoResize && resizeToContents(ref.current);
5835
+ }, []);
5181
5836
  useEffect(() => {
5182
5837
  if (value === localValue) {
5183
5838
  return;
@@ -5195,7 +5850,7 @@ function TextArea(props) {
5195
5850
  id: prefixId$2(id),
5196
5851
  name: id,
5197
5852
  spellCheck: "false",
5198
- class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : ''),
5853
+ class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
5199
5854
  onInput: handleInput,
5200
5855
  onFocus: onFocus,
5201
5856
  onBlur: onBlur,
@@ -5207,20 +5862,20 @@ function TextArea(props) {
5207
5862
  });
5208
5863
  }
5209
5864
 
5210
- /**
5211
- * @param {object} props
5212
- * @param {object} props.element
5213
- * @param {string} props.id
5214
- * @param {string} props.description
5215
- * @param {boolean} props.debounce
5216
- * @param {string} props.label
5217
- * @param {Function} props.getValue
5218
- * @param {Function} props.setValue
5219
- * @param {Function} props.onFocus
5220
- * @param {Function} props.onBlur
5221
- * @param {number} props.rows
5222
- * @param {boolean} props.monospace
5223
- * @param {boolean} [props.disabled]
5865
+ /**
5866
+ * @param {object} props
5867
+ * @param {object} props.element
5868
+ * @param {string} props.id
5869
+ * @param {string} props.description
5870
+ * @param {boolean} props.debounce
5871
+ * @param {string} props.label
5872
+ * @param {Function} props.getValue
5873
+ * @param {Function} props.setValue
5874
+ * @param {Function} props.onFocus
5875
+ * @param {Function} props.onBlur
5876
+ * @param {number} props.rows
5877
+ * @param {boolean} props.monospace
5878
+ * @param {boolean} [props.disabled]
5224
5879
  */
5225
5880
  function TextAreaEntry(props) {
5226
5881
  const {
@@ -5235,7 +5890,8 @@ function TextAreaEntry(props) {
5235
5890
  monospace,
5236
5891
  disabled,
5237
5892
  onFocus,
5238
- onBlur
5893
+ onBlur,
5894
+ autoResize
5239
5895
  } = props;
5240
5896
  const value = getValue(element);
5241
5897
  const error = useError(id);
@@ -5252,7 +5908,8 @@ function TextAreaEntry(props) {
5252
5908
  rows: rows,
5253
5909
  debounce: debounce,
5254
5910
  monospace: monospace,
5255
- disabled: disabled
5911
+ disabled: disabled,
5912
+ autoResize: autoResize
5256
5913
  }, element), error && jsx("div", {
5257
5914
  class: "bio-properties-panel-error",
5258
5915
  children: error
@@ -5323,19 +5980,19 @@ function Textfield(props) {
5323
5980
  });
5324
5981
  }
5325
5982
 
5326
- /**
5327
- * @param {Object} props
5328
- * @param {Object} props.element
5329
- * @param {String} props.id
5330
- * @param {String} props.description
5331
- * @param {Boolean} props.debounce
5332
- * @param {Boolean} props.disabled
5333
- * @param {String} props.label
5334
- * @param {Function} props.getValue
5335
- * @param {Function} props.setValue
5336
- * @param {Function} props.onFocus
5337
- * @param {Function} props.onBlur
5338
- * @param {Function} props.validate
5983
+ /**
5984
+ * @param {Object} props
5985
+ * @param {Object} props.element
5986
+ * @param {String} props.id
5987
+ * @param {String} props.description
5988
+ * @param {Boolean} props.debounce
5989
+ * @param {Boolean} props.disabled
5990
+ * @param {String} props.label
5991
+ * @param {Function} props.getValue
5992
+ * @param {Function} props.setValue
5993
+ * @param {Function} props.onFocus
5994
+ * @param {Function} props.onBlur
5995
+ * @param {Function} props.validate
5339
5996
  */
5340
5997
  function TextfieldEntry(props) {
5341
5998
  const {
@@ -5602,7 +6259,7 @@ function AltTextEntry(props) {
5602
6259
  component: AltText,
5603
6260
  editField: editField,
5604
6261
  field: field,
5605
- isEdited: isEdited$6
6262
+ isEdited: isEdited$7
5606
6263
  });
5607
6264
  }
5608
6265
  return entries;
@@ -5636,59 +6293,70 @@ function AltText(props) {
5636
6293
  });
5637
6294
  }
5638
6295
 
6296
+ const AUTO_OPTION_VALUE = '';
5639
6297
  function ColumnsEntry(props) {
5640
6298
  const {
5641
6299
  editField,
5642
6300
  field
5643
6301
  } = props;
5644
- const {
5645
- type
5646
- } = field;
5647
- const entries = [];
5648
- if (type === 'columns') {
5649
- entries.push({
5650
- id: 'columns',
5651
- component: Columns,
5652
- editField: editField,
5653
- field: field,
5654
- isEdited: isEdited$5
5655
- });
5656
- }
6302
+ const entries = [{
6303
+ id: 'columns',
6304
+ component: Columns,
6305
+ field,
6306
+ editField,
6307
+ isEdited: isEdited$4
6308
+ }];
5657
6309
  return entries;
5658
6310
  }
5659
6311
  function Columns(props) {
5660
6312
  const {
5661
- editField,
5662
6313
  field,
6314
+ editField,
5663
6315
  id
5664
6316
  } = props;
5665
6317
  const debounce = useService('debounce');
5666
- const getValue = () => {
5667
- return field.components.length;
6318
+ const formLayoutValidator = useService('formLayoutValidator');
6319
+ const validate = value => {
6320
+ return formLayoutValidator.validateField(field, value ? parseInt(value) : null);
5668
6321
  };
5669
6322
  const setValue = value => {
5670
- let components = field.components.slice();
5671
- if (value > components.length) {
5672
- while (value > components.length) {
5673
- components.push(Default.create({
5674
- _parent: field.id
5675
- }));
5676
- }
5677
- } else {
5678
- components = components.slice(0, value);
5679
- }
5680
- editField(field, 'components', components);
6323
+ const layout = get(field, ['layout'], {});
6324
+ const newValue = value ? parseInt(value) : null;
6325
+ editField(field, ['layout'], set$1(layout, ['columns'], newValue));
5681
6326
  };
5682
- return NumberFieldEntry({
6327
+ const getValue = () => {
6328
+ return get(field, ['layout', 'columns']);
6329
+ };
6330
+ const getOptions = () => {
6331
+ return [{
6332
+ label: 'Auto',
6333
+ value: AUTO_OPTION_VALUE
6334
+ },
6335
+ // todo(pinussilvestrus): make options dependant on field type
6336
+ // cf. https://github.com/bpmn-io/form-js/issues/575
6337
+ ...[2, 4, 6, 8, 10, 12, 14, 16].map(asOption)];
6338
+ };
6339
+ return SelectEntry({
5683
6340
  debounce,
5684
6341
  element: field,
5685
- getValue,
5686
6342
  id,
5687
6343
  label: 'Columns',
5688
- setValue
6344
+ getOptions,
6345
+ getValue,
6346
+ setValue,
6347
+ validate
5689
6348
  });
5690
6349
  }
5691
6350
 
6351
+ // helper /////////
6352
+
6353
+ function asOption(number) {
6354
+ return {
6355
+ value: number,
6356
+ label: number.toString()
6357
+ };
6358
+ }
6359
+
5692
6360
  function DescriptionEntry(props) {
5693
6361
  const {
5694
6362
  editField,
@@ -5733,6 +6401,7 @@ function Description(props) {
5733
6401
  });
5734
6402
  }
5735
6403
 
6404
+ const EMPTY_OPTION = null;
5736
6405
  function DefaultOptionEntry(props) {
5737
6406
  const {
5738
6407
  editField,
@@ -5878,13 +6547,14 @@ function DefaultValueSingleSelect(props) {
5878
6547
  label
5879
6548
  } = props;
5880
6549
  const {
5881
- defaultValue,
6550
+ defaultValue = EMPTY_OPTION,
5882
6551
  values = []
5883
6552
  } = field;
5884
6553
  const path = ['defaultValue'];
5885
6554
  const getOptions = () => {
5886
6555
  return [{
5887
- label: '<none>'
6556
+ label: '<none>',
6557
+ value: EMPTY_OPTION
5888
6558
  }, ...values];
5889
6559
  };
5890
6560
  const setValue = value => {
@@ -5981,7 +6651,7 @@ function DisabledEntry(props) {
5981
6651
  component: Disabled,
5982
6652
  editField: editField,
5983
6653
  field: field,
5984
- isEdited: isEdited$7
6654
+ isEdited: isEdited$8
5985
6655
  });
5986
6656
  }
5987
6657
  return entries;
@@ -6191,6 +6861,7 @@ function simpleBoolEntryFactory(options) {
6191
6861
  const {
6192
6862
  id,
6193
6863
  label,
6864
+ description,
6194
6865
  path,
6195
6866
  props
6196
6867
  } = options;
@@ -6204,8 +6875,9 @@ function simpleBoolEntryFactory(options) {
6204
6875
  path,
6205
6876
  field,
6206
6877
  editField,
6878
+ description,
6207
6879
  component: SimpleBoolComponent,
6208
- isEdited: isEdited$7
6880
+ isEdited: isEdited$8
6209
6881
  };
6210
6882
  }
6211
6883
  const SimpleBoolComponent = props => {
@@ -6214,7 +6886,8 @@ const SimpleBoolComponent = props => {
6214
6886
  label,
6215
6887
  path,
6216
6888
  field,
6217
- editField
6889
+ editField,
6890
+ description
6218
6891
  } = props;
6219
6892
  const getValue = () => get(field, path, '');
6220
6893
  const setValue = value => editField(field, path, value);
@@ -6223,7 +6896,8 @@ const SimpleBoolComponent = props => {
6223
6896
  getValue,
6224
6897
  id,
6225
6898
  label,
6226
- setValue
6899
+ setValue,
6900
+ description
6227
6901
  });
6228
6902
  };
6229
6903
 
@@ -6279,7 +6953,7 @@ function SourceEntry(props) {
6279
6953
  component: Source,
6280
6954
  editField: editField,
6281
6955
  field: field,
6282
- isEdited: isEdited$6
6956
+ isEdited: isEdited$7
6283
6957
  });
6284
6958
  }
6285
6959
  return entries;
@@ -6317,21 +6991,38 @@ function Source(props) {
6317
6991
  function TextEntry(props) {
6318
6992
  const {
6319
6993
  editField,
6994
+ /* getService, */
6320
6995
  field
6321
6996
  } = props;
6322
6997
  const {
6323
6998
  type
6324
6999
  } = field;
7000
+
7001
+ // const templating = getService('templating');
7002
+
6325
7003
  if (type !== 'text') {
6326
7004
  return [];
6327
7005
  }
6328
- return [{
7006
+ const entries = [{
6329
7007
  id: 'text',
6330
7008
  component: Text,
6331
7009
  editField: editField,
6332
7010
  field: field,
6333
- isEdited: isEdited$6
7011
+ isEdited: isEdited$7
6334
7012
  }];
7013
+
7014
+ // todo: skipped to make the release without too much risk
7015
+ // if (templating.isTemplate(field.text)) {
7016
+ // entries.push(simpleBoolEntryFactory({
7017
+ // id: 'strict',
7018
+ // path: [ 'strict' ],
7019
+ // label: 'Strict templating',
7020
+ // description: 'Enforces types to be correct',
7021
+ // props
7022
+ // }));
7023
+ // }
7024
+
7025
+ return entries;
6335
7026
  }
6336
7027
  function Text(props) {
6337
7028
  const {
@@ -6350,15 +7041,21 @@ function Text(props) {
6350
7041
  const setValue = value => {
6351
7042
  return editField(field, path, value);
6352
7043
  };
6353
- return FeelTextArea({
7044
+ const description = useMemo(() => jsxs(Fragment, {
7045
+ children: ["Supports markdown and templating. ", jsx("a", {
7046
+ href: "https://docs.camunda.io/docs/components/modeler/forms/form-element-library/forms-element-library-text/",
7047
+ target: "_blank",
7048
+ children: "Learn more"
7049
+ })]
7050
+ }), []);
7051
+ return FeelTemplatingEntry({
6354
7052
  debounce,
6355
- description: 'Use an Expression, Markdown or basic HTML to format.',
7053
+ description,
6356
7054
  element: field,
6357
- feel: 'optional',
6358
7055
  getValue,
6359
7056
  id,
6360
7057
  label: 'Text',
6361
- rows: 10,
7058
+ hostLanguage: 'markdown',
6362
7059
  setValue,
6363
7060
  variables
6364
7061
  });
@@ -6474,7 +7171,7 @@ function NumberSerializationEntry(props) {
6474
7171
  entries.push({
6475
7172
  id: 'serialize-to-string',
6476
7173
  component: SerializeToString,
6477
- isEdited: isEdited$7,
7174
+ isEdited: isEdited$8,
6478
7175
  editField,
6479
7176
  field
6480
7177
  });
@@ -6533,7 +7230,7 @@ function DateTimeEntry(props) {
6533
7230
  entries.push({
6534
7231
  id: 'use24h',
6535
7232
  component: Use24h,
6536
- isEdited: isEdited$7,
7233
+ isEdited: isEdited$8,
6537
7234
  editField,
6538
7235
  field
6539
7236
  });
@@ -6646,7 +7343,7 @@ function DateTimeConstraintsEntry(props) {
6646
7343
  entries.push({
6647
7344
  id: id + '-disallowPassedDates',
6648
7345
  component: DisallowPassedDates,
6649
- isEdited: isEdited$7,
7346
+ isEdited: isEdited$8,
6650
7347
  editField,
6651
7348
  field
6652
7349
  });
@@ -7021,11 +7718,19 @@ function InputKeyValuesSourceEntry(props) {
7021
7718
  field,
7022
7719
  id
7023
7720
  } = props;
7721
+ const schema = '[\n {\n "label": "dollar",\n "value": "$"\n }\n]';
7722
+ const description = jsxs("div", {
7723
+ children: ["Define which input property to populate the values from.", jsx("br", {}), jsx("br", {}), "The input property may be an array of simple values or alternatively follow this schema:", jsx("pre", {
7724
+ children: jsx("code", {
7725
+ children: schema
7726
+ })
7727
+ })]
7728
+ });
7024
7729
  return [{
7025
7730
  id: id + '-key',
7026
7731
  component: InputValuesKey,
7027
7732
  label: 'Input values key',
7028
- description: 'Define which input property to populate the values from',
7733
+ description,
7029
7734
  isEdited: isEdited$1,
7030
7735
  editField,
7031
7736
  field
@@ -7220,7 +7925,7 @@ function ConditionEntry(props) {
7220
7925
  component: Condition,
7221
7926
  editField: editField,
7222
7927
  field: field,
7223
- isEdited: isEdited$6
7928
+ isEdited: isEdited$7
7224
7929
  }];
7225
7930
  }
7226
7931
  function Condition(props) {
@@ -7258,7 +7963,7 @@ function Condition(props) {
7258
7963
  });
7259
7964
  }
7260
7965
 
7261
- function GeneralGroup(field, editField) {
7966
+ function GeneralGroup(field, editField, getService) {
7262
7967
  const entries = [...IdEntry({
7263
7968
  field,
7264
7969
  editField
@@ -7277,15 +7982,13 @@ function GeneralGroup(field, editField) {
7277
7982
  }), ...ActionEntry({
7278
7983
  field,
7279
7984
  editField
7280
- }), ...ColumnsEntry({
7281
- field,
7282
- editField
7283
7985
  }), ...DateTimeEntry({
7284
7986
  field,
7285
7987
  editField
7286
7988
  }), ...TextEntry({
7287
7989
  field,
7288
- editField
7990
+ editField,
7991
+ getService
7289
7992
  }), ...NumberEntries({
7290
7993
  field,
7291
7994
  editField
@@ -7381,7 +8084,7 @@ function ValidationGroup(field, editField) {
7381
8084
  component: Required,
7382
8085
  getValue,
7383
8086
  field,
7384
- isEdited: isEdited$7,
8087
+ isEdited: isEdited$8,
7385
8088
  onChange
7386
8089
  }];
7387
8090
  if (type === 'textfield') {
@@ -7723,6 +8426,27 @@ function AppearanceGroup(field, editField) {
7723
8426
  };
7724
8427
  }
7725
8428
 
8429
+ function LayoutGroup(field, editField) {
8430
+ const {
8431
+ type
8432
+ } = field;
8433
+ if (type === 'default') {
8434
+ return null;
8435
+ }
8436
+ const entries = [...ColumnsEntry({
8437
+ field,
8438
+ editField
8439
+ })];
8440
+ if (entries.length === 0) {
8441
+ return null;
8442
+ }
8443
+ return {
8444
+ id: 'layout',
8445
+ label: 'Layout',
8446
+ entries
8447
+ };
8448
+ }
8449
+
7726
8450
  function ConditionGroup(field, editField) {
7727
8451
  const {
7728
8452
  type
@@ -7741,11 +8465,11 @@ function ConditionGroup(field, editField) {
7741
8465
  };
7742
8466
  }
7743
8467
 
7744
- function getGroups(field, editField) {
8468
+ function getGroups(field, editField, getService) {
7745
8469
  if (!field) {
7746
8470
  return [];
7747
8471
  }
7748
- const groups = [GeneralGroup(field, editField), ConditionGroup(field, editField), AppearanceGroup(field, editField), SerializationGroup(field, editField), ...ValuesGroups(field, editField), ConstraintsGroup(field, editField), ValidationGroup(field, editField), CustomValuesGroup(field, editField)];
8472
+ const groups = [GeneralGroup(field, editField, getService), ConditionGroup(field, editField), LayoutGroup(field, editField), AppearanceGroup(field, editField), SerializationGroup(field, editField), ...ValuesGroups(field, editField), ConstraintsGroup(field, editField), ValidationGroup(field, editField), CustomValuesGroup(field, editField)];
7749
8473
 
7750
8474
  // contract: if a group returns null, it should not be displayed at all
7751
8475
  return groups.filter(group => group !== null);
@@ -7798,10 +8522,9 @@ function FormPropertiesPanel(props) {
7798
8522
  };
7799
8523
  }, []);
7800
8524
  const selectedFormField = state.selectedFormField;
8525
+ const getService = (type, strict = true) => injector.get(type, strict);
7801
8526
  const propertiesPanelContext = {
7802
- getService(type, strict = true) {
7803
- return injector.get(type, strict);
7804
- }
8527
+ getService
7805
8528
  };
7806
8529
  const onFocus = () => eventBus.fire('propertiesPanel.focusin');
7807
8530
  const onBlur = () => eventBus.fire('propertiesPanel.focusout');
@@ -7816,7 +8539,7 @@ function FormPropertiesPanel(props) {
7816
8539
  children: jsx(PropertiesPanel, {
7817
8540
  element: selectedFormField,
7818
8541
  eventBus: eventBus,
7819
- groups: getGroups(selectedFormField, editField),
8542
+ groups: getGroups(selectedFormField, editField, getService),
7820
8543
  headerProvider: PropertiesPanelHeaderProvider,
7821
8544
  placeholderProvider: PropertiesPanelPlaceholderProvider
7822
8545
  })
@@ -7894,6 +8617,12 @@ var PropertiesPanelModule = {
7894
8617
  propertiesPanel: ['type', PropertiesPanelRenderer]
7895
8618
  };
7896
8619
 
8620
+ var ExpressionLanguageModule = {
8621
+ __init__: ['expressionLanguage', 'templating'],
8622
+ expressionLanguage: ['type', FeelExpressionLanguage],
8623
+ templating: ['type', FeelersTemplating]
8624
+ };
8625
+
7897
8626
  const ids = new Ids([32, 36, 1]);
7898
8627
 
7899
8628
  /**
@@ -8146,7 +8875,7 @@ class FormEditor {
8146
8875
  * @internal
8147
8876
  */
8148
8877
  _getModules() {
8149
- return [ModelingModule, EditorActionsModule, KeyboardModule, SelectionModule, PaletteModule, PropertiesPanelModule];
8878
+ return [ModelingModule, EditorActionsModule, DraggingModule, KeyboardModule, SelectionModule, PaletteModule, ExpressionLanguageModule, MarkdownModule, PropertiesPanelModule];
8150
8879
  }
8151
8880
 
8152
8881
  /**