@bpmn-io/form-js-editor 0.12.2 → 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 (47) 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 +1161 -60
  4. package/dist/index.cjs +1247 -526
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +1249 -528
  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/InputKeyValuesSourceEntry.d.ts +1 -1
  26. package/dist/types/features/properties-panel/entries/SelectEntries.d.ts +1 -0
  27. package/dist/types/features/properties-panel/entries/factories/simpleBoolEntryFactory.d.ts +1 -0
  28. package/dist/types/features/properties-panel/groups/GeneralGroup.d.ts +1 -1
  29. package/dist/types/features/properties-panel/groups/LayoutGroup.d.ts +11 -0
  30. package/dist/types/features/properties-panel/groups/index.d.ts +1 -0
  31. package/dist/types/features/properties-panel/index.d.ts +1 -1
  32. package/dist/types/features/selection/index.d.ts +2 -2
  33. package/dist/types/import/Importer.d.ts +4 -2
  34. package/dist/types/import/index.d.ts +1 -1
  35. package/dist/types/index.d.ts +2 -2
  36. package/dist/types/render/EditorFormFields.d.ts +1 -1
  37. package/dist/types/render/components/FieldDragPreview.d.ts +1 -0
  38. package/dist/types/render/components/editor-form-fields/EditorText.d.ts +1 -1
  39. package/dist/types/render/components/editor-form-fields/index.d.ts +1 -1
  40. package/dist/types/render/components/icons/index.d.ts +1 -1
  41. package/dist/types/render/hooks/index.d.ts +3 -0
  42. package/dist/types/render/hooks/useDebounce.d.ts +1 -0
  43. package/dist/types/render/hooks/usePrevious.d.ts +1 -0
  44. package/dist/types/render/index.d.ts +2 -2
  45. package/package.json +10 -8
  46. package/dist/assets/dragula.css +0 -22
  47. 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 = {},
@@ -944,6 +1035,307 @@ function unset() {
944
1035
  set(null);
945
1036
  }
946
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
+
947
1339
  function ContextPad(props) {
948
1340
  if (!props.children) {
949
1341
  return null;
@@ -990,7 +1382,7 @@ function Element(props) {
990
1382
  event.stopPropagation();
991
1383
  selection.toggle(field);
992
1384
  }
993
- const classes = ['fjs-element'];
1385
+ const classes = [];
994
1386
  if (props.class) {
995
1387
  classes.push(...props.class.split(' '));
996
1388
  }
@@ -1009,7 +1401,9 @@ function Element(props) {
1009
1401
  "data-field-type": type,
1010
1402
  onClick: onClick,
1011
1403
  ref: ref,
1012
- children: [jsx(ContextPad, {
1404
+ children: [jsx(DebugColumns, {
1405
+ field: field
1406
+ }), jsx(ContextPad, {
1013
1407
  children: selection.isSelected(field) && field.type !== 'default' ? jsx("button", {
1014
1408
  class: "fjs-context-pad-item",
1015
1409
  onClick: onRemove,
@@ -1018,6 +1412,20 @@ function Element(props) {
1018
1412
  }), props.children]
1019
1413
  });
1020
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
+ }
1021
1429
  function Children(props) {
1022
1430
  const {
1023
1431
  field
@@ -1025,7 +1433,7 @@ function Children(props) {
1025
1433
  const {
1026
1434
  id
1027
1435
  } = field;
1028
- const classes = ['fjs-children', 'fjs-drag-container'];
1436
+ const classes = ['fjs-children', DROP_CONTAINER_VERTICAL_CLS];
1029
1437
  if (props.class) {
1030
1438
  classes.push(...props.class.split(' '));
1031
1439
  }
@@ -1035,12 +1443,51 @@ function Children(props) {
1035
1443
  children: props.children
1036
1444
  });
1037
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
+ }
1038
1486
  function FormEditor$1(props) {
1039
- const eventBus = useService$1('eventBus'),
1487
+ const dragging = useService$1('dragging'),
1488
+ eventBus = useService$1('eventBus'),
1040
1489
  formEditor = useService$1('formEditor'),
1041
- formFieldRegistry = useService$1('formFieldRegistry'),
1042
1490
  injector = useService$1('injector'),
1043
- modeling = useService$1('modeling'),
1044
1491
  selection = useService$1('selection'),
1045
1492
  palette = useService$1('palette'),
1046
1493
  paletteConfig = useService$1('config.palette'),
@@ -1049,6 +1496,7 @@ function FormEditor$1(props) {
1049
1496
  const {
1050
1497
  schema
1051
1498
  } = formEditor._getState();
1499
+ const formContainerRef = useRef(null);
1052
1500
  const paletteRef = useRef(null);
1053
1501
  const propertiesPanelRef = useRef(null);
1054
1502
  const [, setSelection] = useState(schema);
@@ -1067,98 +1515,12 @@ function FormEditor$1(props) {
1067
1515
  drake
1068
1516
  };
1069
1517
  useEffect(() => {
1070
- const handleDragEvent = (type, context) => {
1071
- return eventBus.fire(type, context);
1072
- };
1073
- const createDragulaInstance = () => {
1074
- const dragulaInstance = dragula({
1075
- isContainer(el) {
1076
- return el.classList.contains('fjs-drag-container');
1077
- },
1078
- copy(el) {
1079
- return el.classList.contains('fjs-drag-copy');
1080
- },
1081
- accepts(el, target) {
1082
- return !target.classList.contains('fjs-no-drop');
1083
- },
1084
- slideFactorX: 10,
1085
- slideFactorY: 5
1086
- });
1087
-
1088
- // bind life cycle events
1089
- dragulaInstance.on('drag', (element, source) => {
1090
- handleDragEvent('drag.start', {
1091
- element,
1092
- source
1093
- });
1094
- });
1095
- dragulaInstance.on('dragend', element => {
1096
- handleDragEvent('drag.end', {
1097
- element
1098
- });
1099
- });
1100
- dragulaInstance.on('drop', (element, target, source, sibling) => {
1101
- handleDragEvent('drag.drop', {
1102
- element,
1103
- target,
1104
- source,
1105
- sibling
1106
- });
1107
- });
1108
- dragulaInstance.on('over', (element, container, source) => {
1109
- handleDragEvent('drag.hover', {
1110
- element,
1111
- container,
1112
- source
1113
- });
1114
- });
1115
- dragulaInstance.on('out', (element, container, source) => {
1116
- handleDragEvent('drag.out', {
1117
- element,
1118
- container,
1119
- source
1120
- });
1121
- });
1122
- dragulaInstance.on('cancel', (element, container, source) => {
1123
- handleDragEvent('drag.cancel', {
1124
- element,
1125
- container,
1126
- source
1127
- });
1128
- });
1129
-
1130
- // set custom styling
1131
- dragulaInstance.on('drag', () => {
1132
- set('grabbing');
1133
- });
1134
- dragulaInstance.on('dragend', () => {
1135
- unset();
1136
- });
1137
- dragulaInstance.on('drop', (el, target, source, sibling) => {
1138
- dragulaInstance.remove();
1139
- if (!target) {
1140
- return;
1141
- }
1142
- const targetFormField = formFieldRegistry.get(target.dataset.id);
1143
- const siblingFormField = sibling && formFieldRegistry.get(sibling.dataset.id),
1144
- targetIndex = siblingFormField ? getFormFieldIndex(targetFormField, siblingFormField) : targetFormField.components.length;
1145
- if (source.classList.contains('fjs-palette-fields')) {
1146
- const type = el.dataset.fieldType;
1147
- modeling.addFormField({
1148
- type
1149
- }, targetFormField, targetIndex);
1150
- } else {
1151
- const formField = formFieldRegistry.get(el.dataset.id),
1152
- sourceFormField = formFieldRegistry.get(source.dataset.id),
1153
- sourceIndex = getFormFieldIndex(sourceFormField, formField);
1154
- modeling.moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex);
1155
- }
1156
- });
1157
- eventBus.fire('dragula.created');
1158
- setDrake(dragulaInstance);
1159
- return dragulaInstance;
1160
- };
1161
- 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);
1162
1524
  const onDetach = () => {
1163
1525
  if (dragulaInstance) {
1164
1526
  dragulaInstance.destroy();
@@ -1167,14 +1529,34 @@ function FormEditor$1(props) {
1167
1529
  };
1168
1530
  const onAttach = () => {
1169
1531
  onDetach();
1170
- 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();
1171
1547
  };
1172
1548
  eventBus.on('attach', onAttach);
1173
1549
  eventBus.on('detach', onDetach);
1550
+ eventBus.on('dragula.created', onCreate);
1551
+ eventBus.on('drag.start', onDragStart);
1552
+ eventBus.on('drag.end', onDragEnd);
1174
1553
  return () => {
1175
1554
  onDetach();
1176
1555
  eventBus.off('attach', onAttach);
1177
1556
  eventBus.off('detach', onDetach);
1557
+ eventBus.off('dragula.created', onCreate);
1558
+ eventBus.off('drag.start', onDragStart);
1559
+ eventBus.off('drag.end', onDragEnd);
1178
1560
  };
1179
1561
  }, []);
1180
1562
 
@@ -1184,8 +1566,10 @@ function FormEditor$1(props) {
1184
1566
  }, []);
1185
1567
  const formRenderContext = {
1186
1568
  Children,
1569
+ Column,
1187
1570
  Element,
1188
- Empty
1571
+ Empty,
1572
+ Row
1189
1573
  };
1190
1574
  const formContext = {
1191
1575
  getService(type, strict = true) {
@@ -1234,6 +1618,7 @@ function FormEditor$1(props) {
1234
1618
  class: "fjs-editor-palette-container",
1235
1619
  ref: paletteRef
1236
1620
  }), jsx("div", {
1621
+ ref: formContainerRef,
1237
1622
  class: "fjs-form-container",
1238
1623
  children: jsx(FormContext.Provider, {
1239
1624
  value: formContext,
@@ -1269,29 +1654,39 @@ function CreatePreview(props) {
1269
1654
  } = useContext(DragAndDropContext$1);
1270
1655
  function handleCloned(clone, original, type) {
1271
1656
  const fieldType = clone.dataset.fieldType;
1272
- const Icon = iconsByType(fieldType);
1273
- const {
1274
- label
1275
- } = findPaletteEntry(fieldType);
1657
+
1658
+ // (1) field preview
1276
1659
  if (fieldType) {
1660
+ const {
1661
+ label
1662
+ } = findPaletteEntry(fieldType);
1663
+ const Icon = iconsByType(fieldType);
1277
1664
  clone.innerHTML = '';
1278
1665
  clone.class = 'gu-mirror';
1666
+ clone.classList.add('fjs-field-preview-container');
1279
1667
  if (original.classList.contains('fjs-palette-field')) {
1280
- render(jsxs("div", {
1281
- class: "fjs-palette-field",
1282
- children: [jsx(Icon, {
1283
- class: "fjs-palette-field-icon",
1284
- width: "36",
1285
- height: "36",
1286
- viewBox: "0 0 54 54"
1287
- }), jsx("span", {
1288
- class: "fjs-palette-field-text",
1289
- children: label
1290
- })]
1291
- }), clone);
1292
- } else {
1293
- render(jsx(Icon, {}), clone);
1668
+ // default to auto columns when creating from palette
1669
+ clone.classList.add('cds--col');
1294
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');
1295
1690
  }
1296
1691
  }
1297
1692
  useEffect(() => {
@@ -1364,10 +1759,12 @@ var renderModule = {
1364
1759
 
1365
1760
  var core = {
1366
1761
  __depends__: [importModule, renderModule],
1762
+ debounce: ['factory', DebounceFactory],
1367
1763
  eventBus: ['type', EventBus],
1368
1764
  formFieldRegistry: ['type', FormFieldRegistry],
1369
- fieldFactory: ['type', FieldFactory],
1370
- debounce: ['factory', DebounceFactory]
1765
+ formLayouter: ['type', FormLayouter],
1766
+ formLayoutValidator: ['type', FormLayoutValidator],
1767
+ fieldFactory: ['type', FieldFactory]
1371
1768
  };
1372
1769
 
1373
1770
  var NOT_REGISTERED_ERROR = 'is not a registered action',
@@ -1645,6 +2042,11 @@ var EditorActionsModule = {
1645
2042
  editorActions: ['type', FormEditorActions]
1646
2043
  };
1647
2044
 
2045
+ var DraggingModule = {
2046
+ __init__: ['dragging'],
2047
+ dragging: ['type', Dragging]
2048
+ };
2049
+
1648
2050
  var KEYS_COPY = ['c', 'C', 'KeyC'];
1649
2051
  var KEYS_PASTE = ['v', 'V', 'KeyV'];
1650
2052
  var KEYS_REDO$1 = ['y', 'Y', 'KeyY'];
@@ -2057,6 +2459,13 @@ function updatePath(formFieldRegistry, formField, index) {
2057
2459
  formField._path = [...parent._path, 'components', index];
2058
2460
  return formField;
2059
2461
  }
2462
+ function updateRow(formField, rowId) {
2463
+ formField.layout = {
2464
+ ...(formField.layout || {}),
2465
+ row: rowId
2466
+ };
2467
+ return formField;
2468
+ }
2060
2469
 
2061
2470
  class AddFormFieldHandler {
2062
2471
  /**
@@ -2206,13 +2615,17 @@ class MoveFormFieldHandler {
2206
2615
  sourceFormField,
2207
2616
  targetFormField,
2208
2617
  sourceIndex,
2209
- targetIndex
2618
+ targetIndex,
2619
+ sourceRow,
2620
+ targetRow
2210
2621
  } = context;
2211
2622
  this.moveFormField({
2212
2623
  sourceFormField: targetFormField,
2213
2624
  targetFormField: sourceFormField,
2214
2625
  sourceIndex: targetIndex,
2215
- targetIndex: sourceIndex
2626
+ targetIndex: sourceIndex,
2627
+ sourceRow: targetRow,
2628
+ targetRow: sourceRow
2216
2629
  }, true);
2217
2630
  }
2218
2631
  moveFormField(context, revert) {
@@ -2220,7 +2633,8 @@ class MoveFormFieldHandler {
2220
2633
  sourceFormField,
2221
2634
  targetFormField,
2222
2635
  sourceIndex,
2223
- targetIndex
2636
+ targetIndex,
2637
+ targetRow
2224
2638
  } = context;
2225
2639
  let {
2226
2640
  schema
@@ -2236,11 +2650,15 @@ class MoveFormFieldHandler {
2236
2650
  targetIndex--;
2237
2651
  }
2238
2652
  }
2653
+ const formField = get(schema, [...sourcePath, sourceIndex]);
2654
+
2655
+ // (1) Add to row
2656
+ updateRow(formField, targetRow ? targetRow.id : null);
2239
2657
 
2240
- // (1) Move form field
2658
+ // (2) Move form field
2241
2659
  mutate(get(schema, sourcePath), sourceIndex, targetIndex);
2242
2660
 
2243
- // (2) Update paths of new form field and its siblings
2661
+ // (3) Update paths of new form field and its siblings
2244
2662
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
2245
2663
  } else {
2246
2664
  const formField = get(schema, [...sourcePath, sourceIndex]);
@@ -2253,10 +2671,13 @@ class MoveFormFieldHandler {
2253
2671
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
2254
2672
  const targetPath = [...targetFormField._path, 'components'];
2255
2673
 
2256
- // (3) Add form field
2674
+ // (3) Add to row
2675
+ updateRow(formField, targetRow ? targetRow.id : null);
2676
+
2677
+ // (4) Add form field
2257
2678
  arrayAdd$1(get(schema, targetPath), targetIndex, formField);
2258
2679
 
2259
- // (4) Update paths of siblings
2680
+ // (5) Update paths of siblings
2260
2681
  get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
2261
2682
  }
2262
2683
 
@@ -2448,13 +2869,15 @@ class Modeling {
2448
2869
  };
2449
2870
  this._commandStack.execute('formField.edit', context);
2450
2871
  }
2451
- moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex) {
2872
+ moveFormField(formField, sourceFormField, targetFormField, sourceIndex, targetIndex, sourceRow, targetRow) {
2452
2873
  const context = {
2453
2874
  formField,
2454
2875
  sourceFormField,
2455
2876
  targetFormField,
2456
2877
  sourceIndex,
2457
- targetIndex
2878
+ targetIndex,
2879
+ sourceRow,
2880
+ targetRow
2458
2881
  };
2459
2882
  this._commandStack.execute('formField.move', context);
2460
2883
  }
@@ -2613,6 +3036,44 @@ forEach(hooks, function (hook) {
2613
3036
  };
2614
3037
  });
2615
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
+
2616
3077
  class IdBehavior extends CommandInterceptor {
2617
3078
  constructor(eventBus, modeling) {
2618
3079
  super(eventBus);
@@ -3134,7 +3595,8 @@ var commandModule = {
3134
3595
 
3135
3596
  var ModelingModule = {
3136
3597
  __depends__: [behaviorModule, commandModule],
3137
- __init__: ['modeling'],
3598
+ __init__: ['formLayoutUpdater', 'modeling'],
3599
+ formLayoutUpdater: ['type', FormLayoutUpdater],
3138
3600
  modeling: ['type', Modeling]
3139
3601
  };
3140
3602
 
@@ -3430,19 +3892,19 @@ const ErrorsContext = createContext({
3430
3892
  errors: {}
3431
3893
  });
3432
3894
 
3433
- /**
3434
- * @typedef {Function} <propertiesPanel.showEntry> callback
3435
- *
3436
- * @example
3437
- *
3438
- * useEvent('propertiesPanel.showEntry', ({ focus = false, ...rest }) => {
3439
- * // ...
3440
- * });
3441
- *
3442
- * @param {Object} context
3443
- * @param {boolean} [context.focus]
3444
- *
3445
- * @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
3446
3908
  */
3447
3909
  const EventContext = createContext({
3448
3910
  eventBus: null
@@ -3454,20 +3916,20 @@ const LayoutContext = createContext({
3454
3916
  setLayoutForKey: () => {}
3455
3917
  });
3456
3918
 
3457
- /**
3458
- * Accesses the global DescriptionContext and returns a description for a given id and element.
3459
- *
3460
- * @example
3461
- * ```jsx
3462
- * function TextField(props) {
3463
- * const description = useDescriptionContext('input1', element);
3464
- * }
3465
- * ```
3466
- *
3467
- * @param {string} id
3468
- * @param {object} element
3469
- *
3470
- * @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}
3471
3933
  */
3472
3934
  function useDescriptionContext(id, element) {
3473
3935
  const {
@@ -3482,11 +3944,11 @@ function useError(id) {
3482
3944
  return errors[id];
3483
3945
  }
3484
3946
 
3485
- /**
3486
- * Subscribe to an event immediately. Update subscription after inputs changed.
3487
- *
3488
- * @param {string} event
3489
- * @param {Function} callback
3947
+ /**
3948
+ * Subscribe to an event immediately. Update subscription after inputs changed.
3949
+ *
3950
+ * @param {string} event
3951
+ * @param {Function} callback
3490
3952
  */
3491
3953
  function useEvent(event, callback, eventBus) {
3492
3954
  const eventContext = useContext(EventContext);
@@ -3516,20 +3978,20 @@ function useEvent(event, callback, eventBus) {
3516
3978
  }, [callback, event, eventBus]);
3517
3979
  }
3518
3980
 
3519
- /**
3520
- * Creates a state that persists in the global LayoutContext.
3521
- *
3522
- * @example
3523
- * ```jsx
3524
- * function Group(props) {
3525
- * const [ open, setOpen ] = useLayoutState([ 'groups', 'foo', 'open' ], false);
3526
- * }
3527
- * ```
3528
- *
3529
- * @param {(string|number)[]} path
3530
- * @param {any} [defaultValue]
3531
- *
3532
- * @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 ]}
3533
3995
  */
3534
3996
  function useLayoutState(path, defaultValue) {
3535
3997
  const {
@@ -3537,22 +3999,17 @@ function useLayoutState(path, defaultValue) {
3537
3999
  setLayoutForKey
3538
4000
  } = useContext(LayoutContext);
3539
4001
  const layoutForKey = getLayoutForKey(path, defaultValue);
3540
- const [value, set] = useState(layoutForKey);
3541
- const setState = newValue => {
3542
- // (1) set component state
3543
- set(newValue);
3544
-
3545
- // (2) set context
4002
+ const setState = useCallback(newValue => {
3546
4003
  setLayoutForKey(path, newValue);
3547
- };
3548
- return [value, setState];
4004
+ }, [setLayoutForKey]);
4005
+ return [layoutForKey, setState];
3549
4006
  }
3550
4007
 
3551
- /**
3552
- * @pinussilvestrus: we need to introduce our own hook to persist the previous
3553
- * state on updates.
3554
- *
3555
- * 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
3556
4013
  */
3557
4014
 
3558
4015
  function usePrevious(value) {
@@ -3563,12 +4020,12 @@ function usePrevious(value) {
3563
4020
  return ref.current;
3564
4021
  }
3565
4022
 
3566
- /**
3567
- * Subscribe to `propertiesPanel.showEntry`.
3568
- *
3569
- * @param {string} id
3570
- *
3571
- * @returns {import('preact').Ref}
4023
+ /**
4024
+ * Subscribe to `propertiesPanel.showEntry`.
4025
+ *
4026
+ * @param {string} id
4027
+ *
4028
+ * @returns {import('preact').Ref}
3572
4029
  */
3573
4030
  function useShowEntryEvent(id) {
3574
4031
  const {
@@ -3599,20 +4056,20 @@ function useShowEntryEvent(id) {
3599
4056
  return ref;
3600
4057
  }
3601
4058
 
3602
- /**
3603
- * @callback setSticky
3604
- * @param {boolean} value
4059
+ /**
4060
+ * @callback setSticky
4061
+ * @param {boolean} value
3605
4062
  */
3606
4063
 
3607
- /**
3608
- * Use IntersectionObserver to identify when DOM element is in sticky mode.
3609
- * If sticky is observered setSticky(true) will be called.
3610
- * If sticky mode is left, setSticky(false) will be called.
3611
- *
3612
- *
3613
- * @param {Object} ref
3614
- * @param {string} scrollContainerSelector
3615
- * @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
3616
4073
  */
3617
4074
  function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky) {
3618
4075
  useEffect(() => {
@@ -3651,19 +4108,19 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
3651
4108
  }, [ref, scrollContainerSelector, setSticky]);
3652
4109
  }
3653
4110
 
3654
- /**
3655
- * Creates a static function reference with changing body.
3656
- * This is necessary when external libraries require a callback function
3657
- * that has references to state variables.
3658
- *
3659
- * Usage:
3660
- * const callback = useStaticCallback((val) => {val === currentState});
3661
- *
3662
- * The `callback` reference is static and can be safely used in external
3663
- * libraries or as a prop that does not cause rerendering of children.
3664
- *
3665
- * @param {Function} callback function with changing reference
3666
- * @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
3667
4124
  */
3668
4125
  function useStaticCallback(callback) {
3669
4126
  const callbackRef = useRef(callback);
@@ -3755,13 +4212,13 @@ function DataMarker() {
3755
4212
  });
3756
4213
  }
3757
4214
 
3758
- /**
3759
- * @typedef { {
3760
- * text: (element: object) => string,
3761
- * icon?: (element: Object) => import('preact').Component
3762
- * } } PlaceholderDefinition
3763
- *
3764
- * @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
3765
4222
  */
3766
4223
  function Placeholder(props) {
3767
4224
  const {
@@ -3786,72 +4243,72 @@ const DEFAULT_LAYOUT = {
3786
4243
  };
3787
4244
  const DEFAULT_DESCRIPTION = {};
3788
4245
 
3789
- /**
3790
- * @typedef { {
3791
- * component: import('preact').Component,
3792
- * id: String,
3793
- * isEdited?: Function
3794
- * } } EntryDefinition
3795
- *
3796
- * @typedef { {
3797
- * autoFocusEntry: String,
3798
- * autoOpen?: Boolean,
3799
- * entries: Array<EntryDefinition>,
3800
- * id: String,
3801
- * label: String,
3802
- * remove: (event: MouseEvent) => void
3803
- * } } ListItemDefinition
3804
- *
3805
- * @typedef { {
3806
- * add: (event: MouseEvent) => void,
3807
- * component: import('preact').Component,
3808
- * element: Object,
3809
- * id: String,
3810
- * items: Array<ListItemDefinition>,
3811
- * label: String,
3812
- * shouldSort?: Boolean,
3813
- * shouldOpen?: Boolean
3814
- * } } ListGroupDefinition
3815
- *
3816
- * @typedef { {
3817
- * component?: import('preact').Component,
3818
- * entries: Array<EntryDefinition>,
3819
- * id: String,
3820
- * label: String,
3821
- * shouldOpen?: Boolean
3822
- * } } GroupDefinition
3823
- *
3824
- * @typedef { {
3825
- * [id: String]: GetDescriptionFunction
3826
- * } } DescriptionConfig
3827
- *
3828
- * @callback { {
3829
- * @param {string} id
3830
- * @param {Object} element
3831
- * @returns {string}
3832
- * } } GetDescriptionFunction
3833
- *
3834
- * @typedef { {
3835
- * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
3836
- * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
3837
- * } } PlaceholderProvider
3838
- *
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
+ *
3839
4296
  */
3840
4297
 
3841
- /**
3842
- * A basic properties panel component. Describes *how* content will be rendered, accepts
3843
- * data from implementor to describe *what* will be rendered.
3844
- *
3845
- * @param {Object} props
3846
- * @param {Object|Array} props.element
3847
- * @param {import('./components/Header').HeaderProvider} props.headerProvider
3848
- * @param {PlaceholderProvider} [props.placeholderProvider]
3849
- * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
3850
- * @param {Object} [props.layoutConfig]
3851
- * @param {Function} [props.layoutChanged]
3852
- * @param {DescriptionConfig} [props.descriptionConfig]
3853
- * @param {Function} [props.descriptionLoaded]
3854
- * @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]
3855
4312
  */
3856
4313
  function PropertiesPanel(props) {
3857
4314
  const {
@@ -3859,15 +4316,21 @@ function PropertiesPanel(props) {
3859
4316
  headerProvider,
3860
4317
  placeholderProvider,
3861
4318
  groups,
3862
- layoutConfig = {},
4319
+ layoutConfig,
3863
4320
  layoutChanged,
3864
- descriptionConfig = {},
4321
+ descriptionConfig,
3865
4322
  descriptionLoaded,
3866
4323
  eventBus
3867
4324
  } = props;
3868
4325
 
3869
4326
  // set-up layout context
3870
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]);
3871
4334
  useEffect(() => {
3872
4335
  if (typeof layoutChanged === 'function') {
3873
4336
  layoutChanged(layout);
@@ -3889,10 +4352,12 @@ function PropertiesPanel(props) {
3889
4352
  };
3890
4353
 
3891
4354
  // set-up description context
3892
- const description = createDescriptionContext(descriptionConfig);
3893
- if (typeof descriptionLoaded === 'function') {
3894
- descriptionLoaded(description);
3895
- }
4355
+ const description = useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
4356
+ useEffect(() => {
4357
+ if (typeof descriptionLoaded === 'function') {
4358
+ descriptionLoaded(description);
4359
+ }
4360
+ }, [description, descriptionLoaded]);
3896
4361
  const getDescriptionForId = (id, element) => {
3897
4362
  return description[id] && description[id](element);
3898
4363
  };
@@ -3967,18 +4432,37 @@ function PropertiesPanel(props) {
3967
4432
 
3968
4433
  // helpers //////////////////
3969
4434
 
3970
- function createLayout(overrides) {
4435
+ function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
3971
4436
  return {
3972
- ...DEFAULT_LAYOUT,
4437
+ ...defaults,
3973
4438
  ...overrides
3974
4439
  };
3975
4440
  }
3976
- function createDescriptionContext(overrides) {
4441
+ function createDescriptionContext(overrides = {}) {
3977
4442
  return {
3978
4443
  ...DEFAULT_DESCRIPTION,
3979
4444
  ...overrides
3980
4445
  };
3981
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
+ }
3982
4466
  function CollapsibleEntry(props) {
3983
4467
  const {
3984
4468
  element,
@@ -4074,10 +4558,10 @@ function ListItem(props) {
4074
4558
  })
4075
4559
  });
4076
4560
  }
4077
- const noop$2 = () => {};
4561
+ const noop$3 = () => {};
4078
4562
 
4079
- /**
4080
- * @param {import('../PropertiesPanel').ListGroupDefinition} props
4563
+ /**
4564
+ * @param {import('../PropertiesPanel').ListGroupDefinition} props
4081
4565
  */
4082
4566
  function ListGroup(props) {
4083
4567
  const {
@@ -4095,6 +4579,9 @@ function ListGroup(props) {
4095
4579
  const onShow = useCallback(() => setOpen(true), [setOpen]);
4096
4580
  const [ordering, setOrdering] = useState([]);
4097
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);
4098
4585
  const prevItems = usePrevious(items);
4099
4586
  const prevElement = usePrevious(element);
4100
4587
  const elementChanged = element !== prevElement;
@@ -4116,6 +4603,8 @@ function ListGroup(props) {
4116
4603
 
4117
4604
  // (1) items were added
4118
4605
  useEffect(() => {
4606
+ // reset addTriggered flag
4607
+ setAddTriggered(false);
4119
4608
  if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
4120
4609
  let add = [];
4121
4610
  items.forEach(item => {
@@ -4125,14 +4614,18 @@ function ListGroup(props) {
4125
4614
  });
4126
4615
  let newOrdering = ordering;
4127
4616
 
4128
- // open if not open and configured
4129
- 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) {
4130
4623
  toggleOpen();
4624
+ }
4131
4625
 
4132
- // if I opened and I should sort, then sort items
4133
- if (shouldSort) {
4134
- newOrdering = createOrdering(sortItems(items));
4135
- }
4626
+ // filter when not open and configured
4627
+ if (!open && shouldSort) {
4628
+ newOrdering = createOrdering(sortItems(items));
4136
4629
  }
4137
4630
 
4138
4631
  // add new items on top or bottom depending on sorting behavior
@@ -4143,11 +4636,11 @@ function ListGroup(props) {
4143
4636
  newOrdering.push(...add);
4144
4637
  }
4145
4638
  setOrdering(newOrdering);
4146
- setNewItemAdded(true);
4639
+ setNewItemAdded(addTriggered);
4147
4640
  } else {
4148
4641
  setNewItemAdded(false);
4149
4642
  }
4150
- }, [items, open, shouldHandleEffects]);
4643
+ }, [items, open, shouldHandleEffects, addTriggered]);
4151
4644
 
4152
4645
  // (2) sort items on open if shouldSort is set
4153
4646
  useEffect(() => {
@@ -4177,13 +4670,17 @@ function ListGroup(props) {
4177
4670
  ...useContext(LayoutContext),
4178
4671
  onShow
4179
4672
  };
4673
+ const handleAddClick = e => {
4674
+ setAddTriggered(true);
4675
+ add(e);
4676
+ };
4180
4677
  return jsxs("div", {
4181
4678
  class: "bio-properties-panel-group",
4182
4679
  "data-group-id": 'group-' + id,
4183
4680
  ref: groupRef,
4184
4681
  children: [jsxs("div", {
4185
4682
  class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
4186
- onClick: hasItems ? toggleOpen : noop$2,
4683
+ onClick: hasItems ? toggleOpen : noop$3,
4187
4684
  children: [jsx("div", {
4188
4685
  title: label,
4189
4686
  class: "bio-properties-panel-group-header-title",
@@ -4193,7 +4690,7 @@ function ListGroup(props) {
4193
4690
  children: [add ? jsxs("button", {
4194
4691
  title: "Create new list item",
4195
4692
  class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
4196
- onClick: add,
4693
+ onClick: handleAddClick,
4197
4694
  children: [jsx(CreateIcon, {}), !hasItems ? jsx("span", {
4198
4695
  class: "bio-properties-panel-add-entry-label",
4199
4696
  children: "Create"
@@ -4223,8 +4720,9 @@ function ListGroup(props) {
4223
4720
  id
4224
4721
  } = item;
4225
4722
 
4226
- // if item was added, open first or last item based on ordering
4227
- 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;
4228
4726
  return createElement(ListItem, {
4229
4727
  ...item,
4230
4728
  autoOpen: autoOpen,
@@ -4240,8 +4738,8 @@ function ListGroup(props) {
4240
4738
 
4241
4739
  // helpers ////////////////////
4242
4740
 
4243
- /**
4244
- * Sorts given items alphanumeric by label
4741
+ /**
4742
+ * Sorts given items alphanumeric by label
4245
4743
  */
4246
4744
  function sortItems(items) {
4247
4745
  return sortBy(items, i => i.label.toLowerCase());
@@ -4315,17 +4813,17 @@ function Checkbox(props) {
4315
4813
  });
4316
4814
  }
4317
4815
 
4318
- /**
4319
- * @param {Object} props
4320
- * @param {Object} props.element
4321
- * @param {String} props.id
4322
- * @param {String} props.description
4323
- * @param {String} props.label
4324
- * @param {Function} props.getValue
4325
- * @param {Function} props.setValue
4326
- * @param {Function} props.onFocus
4327
- * @param {Function} props.onBlur
4328
- * @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]
4329
4827
  */
4330
4828
  function CheckboxEntry(props) {
4331
4829
  const {
@@ -4362,7 +4860,7 @@ function CheckboxEntry(props) {
4362
4860
  })]
4363
4861
  });
4364
4862
  }
4365
- function isEdited$7(node) {
4863
+ function isEdited$8(node) {
4366
4864
  return node && !!node.checked;
4367
4865
  }
4368
4866
 
@@ -4371,6 +4869,87 @@ function isEdited$7(node) {
4371
4869
  function prefixId$7(id) {
4372
4870
  return `bio-properties-panel-${id}`;
4373
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
+ });
4374
4953
  const useBufferedFocus = function (editor, ref) {
4375
4954
  const [buffer, setBuffer] = useState(undefined);
4376
4955
  ref.current = useMemo(() => ({
@@ -4413,10 +4992,10 @@ const CodeEditor = forwardRef((props, ref) => {
4413
4992
  useEffect(() => {
4414
4993
  let editor;
4415
4994
 
4416
- /* Trigger FEEL toggle when
4417
- *
4418
- * - `backspace` is pressed
4419
- * - 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
4420
4999
  */
4421
5000
  const onKeyDown = e => {
4422
5001
  if (e.key !== 'Backspace' || !editor) {
@@ -4485,12 +5064,12 @@ function FeelIndicator(props) {
4485
5064
  children: "="
4486
5065
  });
4487
5066
  }
4488
- const noop$1 = () => {};
5067
+ const noop$2 = () => {};
4489
5068
 
4490
- /**
4491
- * @param {Object} props
4492
- * @param {Object} props.label
4493
- * @param {String} props.feel
5069
+ /**
5070
+ * @param {Object} props
5071
+ * @param {Object} props.label
5072
+ * @param {String} props.feel
4494
5073
  */
4495
5074
  function FeelIcon(props) {
4496
5075
  const {
@@ -4498,7 +5077,7 @@ function FeelIcon(props) {
4498
5077
  feel = false,
4499
5078
  active,
4500
5079
  disabled = false,
4501
- onClick = noop$1
5080
+ onClick = noop$2
4502
5081
  } = props;
4503
5082
  const feelRequiredLabel = ' must be a FEEL expression';
4504
5083
  const feelOptionalLabel = ' can optionally be a FEEL expression';
@@ -4518,7 +5097,7 @@ function FeelIcon(props) {
4518
5097
  children: feel === 'required' ? jsx(FeelRequiredIcon, {}) : jsx(FeelOptionalIcon, {})
4519
5098
  });
4520
5099
  }
4521
- const noop = () => {};
5100
+ const noop$1 = () => {};
4522
5101
  function FeelTextfield(props) {
4523
5102
  const {
4524
5103
  debounce,
@@ -4675,6 +5254,9 @@ function FeelTextfield(props) {
4675
5254
  }) : jsx(OptionalComponent, {
4676
5255
  ...props,
4677
5256
  onInput: handleLocalInput,
5257
+ contentAttributes: {
5258
+ 'id': prefixId$6(id)
5259
+ },
4678
5260
  value: localValue,
4679
5261
  ref: editorRef
4680
5262
  })]
@@ -4724,7 +5306,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
4724
5306
  value: value || ''
4725
5307
  });
4726
5308
  });
4727
- const OptionalFeelTextArea = forwardRef((props, ref) => {
5309
+ forwardRef((props, ref) => {
4728
5310
  const {
4729
5311
  id,
4730
5312
  disabled,
@@ -4764,17 +5346,24 @@ const OptionalFeelTextArea = forwardRef((props, ref) => {
4764
5346
  });
4765
5347
  });
4766
5348
 
4767
- /**
4768
- * @param {Object} props
4769
- * @param {Object} props.element
4770
- * @param {String} props.id
4771
- * @param {String} props.description
4772
- * @param {Boolean} props.debounce
4773
- * @param {Boolean} props.disabled
4774
- * @param {String} props.label
4775
- * @param {Function} props.getValue
4776
- * @param {Function} props.setValue
4777
- * @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
4778
5367
  */
4779
5368
  function FeelEntry(props) {
4780
5369
  const {
@@ -4788,8 +5377,10 @@ function FeelEntry(props) {
4788
5377
  getValue,
4789
5378
  setValue,
4790
5379
  tooltipContainer,
5380
+ hostLanguage,
5381
+ singleLine,
4791
5382
  validate,
4792
- show = noop,
5383
+ show = noop$1,
4793
5384
  example,
4794
5385
  variables,
4795
5386
  onFocus,
@@ -4843,6 +5434,8 @@ function FeelEntry(props) {
4843
5434
  onFocus: onFocus,
4844
5435
  onBlur: onBlur,
4845
5436
  example: example,
5437
+ hostLanguage: hostLanguage,
5438
+ singleLine: singleLine,
4846
5439
  show: show,
4847
5440
  value: value,
4848
5441
  variables: variables,
@@ -4859,28 +5452,35 @@ function FeelEntry(props) {
4859
5452
  });
4860
5453
  }
4861
5454
 
4862
- /**
4863
- * @param {Object} props
4864
- * @param {Object} props.element
4865
- * @param {String} props.id
4866
- * @param {String} props.description
4867
- * @param {Boolean} props.debounce
4868
- * @param {Boolean} props.disabled
4869
- * @param {String} props.label
4870
- * @param {Function} props.getValue
4871
- * @param {Function} props.setValue
4872
- * @param {Function} props.onFocus
4873
- * @param {Function} props.onBlur
4874
- * @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
4875
5475
  */
4876
- function FeelTextArea(props) {
5476
+ function FeelTemplatingEntry(props) {
4877
5477
  return jsx(FeelEntry, {
4878
- class: "bio-properties-panel-feel-textarea",
4879
- OptionalComponent: OptionalFeelTextArea,
5478
+ class: "bio-properties-panel-feel-templating",
5479
+ OptionalComponent: CodeEditor$1,
4880
5480
  ...props
4881
5481
  });
4882
5482
  }
4883
- function isEdited$6(node) {
5483
+ function isEdited$7(node) {
4884
5484
  return node && (!!node.value || node.classList.contains('edited'));
4885
5485
  }
4886
5486
 
@@ -4950,22 +5550,22 @@ function NumberField(props) {
4950
5550
  });
4951
5551
  }
4952
5552
 
4953
- /**
4954
- * @param {Object} props
4955
- * @param {Boolean} props.debounce
4956
- * @param {String} props.description
4957
- * @param {Boolean} props.disabled
4958
- * @param {Object} props.element
4959
- * @param {Function} props.getValue
4960
- * @param {String} props.id
4961
- * @param {String} props.label
4962
- * @param {String} props.max
4963
- * @param {String} props.min
4964
- * @param {Function} props.setValue
4965
- * @param {Function} props.onFocus
4966
- * @param {Function} props.onBlur
4967
- * @param {String} props.step
4968
- * @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
4969
5569
  */
4970
5570
  function NumberFieldEntry(props) {
4971
5571
  const {
@@ -5090,6 +5690,16 @@ function Select(props) {
5090
5690
  value: localValue,
5091
5691
  disabled: disabled,
5092
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
+ }
5093
5703
  return jsx("option", {
5094
5704
  value: option.value,
5095
5705
  disabled: option.disabled,
@@ -5100,18 +5710,19 @@ function Select(props) {
5100
5710
  });
5101
5711
  }
5102
5712
 
5103
- /**
5104
- * @param {object} props
5105
- * @param {object} props.element
5106
- * @param {string} props.id
5107
- * @param {string} [props.description]
5108
- * @param {string} props.label
5109
- * @param {Function} props.getValue
5110
- * @param {Function} props.setValue
5111
- * @param {Function} props.onFocus
5112
- * @param {Function} props.onBlur
5113
- * @param {Function} props.getOptions
5114
- * @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]
5115
5726
  */
5116
5727
  function SelectEntry(props) {
5117
5728
  const {
@@ -5124,11 +5735,37 @@ function SelectEntry(props) {
5124
5735
  getOptions,
5125
5736
  disabled,
5126
5737
  onFocus,
5127
- onBlur
5738
+ onBlur,
5739
+ validate
5128
5740
  } = props;
5129
- const value = getValue(element);
5130
5741
  const options = getOptions(element);
5131
- 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;
5132
5769
  return jsxs("div", {
5133
5770
  class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
5134
5771
  "data-entry-id": id,
@@ -5136,7 +5773,7 @@ function SelectEntry(props) {
5136
5773
  id: id,
5137
5774
  label: label,
5138
5775
  value: value,
5139
- onChange: setValue,
5776
+ onChange: onChange,
5140
5777
  onFocus: onFocus,
5141
5778
  onBlur: onBlur,
5142
5779
  options: options,
@@ -5160,18 +5797,26 @@ function isEdited$4(node) {
5160
5797
  function prefixId$4(id) {
5161
5798
  return `bio-properties-panel-${id}`;
5162
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
+ }
5163
5807
  function TextArea(props) {
5164
5808
  const {
5165
5809
  id,
5166
5810
  label,
5167
- rows = 2,
5168
5811
  debounce,
5169
5812
  onInput,
5170
5813
  value = '',
5171
5814
  disabled,
5172
5815
  monospace,
5173
5816
  onFocus,
5174
- onBlur
5817
+ onBlur,
5818
+ autoResize,
5819
+ rows = autoResize ? 1 : 2
5175
5820
  } = props;
5176
5821
  const [localValue, setLocalValue] = useState(value);
5177
5822
  const ref = useShowEntryEvent(id);
@@ -5182,8 +5827,12 @@ function TextArea(props) {
5182
5827
  }, [onInput, debounce]);
5183
5828
  const handleInput = e => {
5184
5829
  handleInputCallback(e);
5830
+ autoResize && resizeToContents(e.target);
5185
5831
  setLocalValue(e.target.value);
5186
5832
  };
5833
+ useLayoutEffect(() => {
5834
+ autoResize && resizeToContents(ref.current);
5835
+ }, []);
5187
5836
  useEffect(() => {
5188
5837
  if (value === localValue) {
5189
5838
  return;
@@ -5201,7 +5850,7 @@ function TextArea(props) {
5201
5850
  id: prefixId$2(id),
5202
5851
  name: id,
5203
5852
  spellCheck: "false",
5204
- 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' : ''),
5205
5854
  onInput: handleInput,
5206
5855
  onFocus: onFocus,
5207
5856
  onBlur: onBlur,
@@ -5213,20 +5862,20 @@ function TextArea(props) {
5213
5862
  });
5214
5863
  }
5215
5864
 
5216
- /**
5217
- * @param {object} props
5218
- * @param {object} props.element
5219
- * @param {string} props.id
5220
- * @param {string} props.description
5221
- * @param {boolean} props.debounce
5222
- * @param {string} props.label
5223
- * @param {Function} props.getValue
5224
- * @param {Function} props.setValue
5225
- * @param {Function} props.onFocus
5226
- * @param {Function} props.onBlur
5227
- * @param {number} props.rows
5228
- * @param {boolean} props.monospace
5229
- * @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]
5230
5879
  */
5231
5880
  function TextAreaEntry(props) {
5232
5881
  const {
@@ -5241,7 +5890,8 @@ function TextAreaEntry(props) {
5241
5890
  monospace,
5242
5891
  disabled,
5243
5892
  onFocus,
5244
- onBlur
5893
+ onBlur,
5894
+ autoResize
5245
5895
  } = props;
5246
5896
  const value = getValue(element);
5247
5897
  const error = useError(id);
@@ -5258,7 +5908,8 @@ function TextAreaEntry(props) {
5258
5908
  rows: rows,
5259
5909
  debounce: debounce,
5260
5910
  monospace: monospace,
5261
- disabled: disabled
5911
+ disabled: disabled,
5912
+ autoResize: autoResize
5262
5913
  }, element), error && jsx("div", {
5263
5914
  class: "bio-properties-panel-error",
5264
5915
  children: error
@@ -5329,19 +5980,19 @@ function Textfield(props) {
5329
5980
  });
5330
5981
  }
5331
5982
 
5332
- /**
5333
- * @param {Object} props
5334
- * @param {Object} props.element
5335
- * @param {String} props.id
5336
- * @param {String} props.description
5337
- * @param {Boolean} props.debounce
5338
- * @param {Boolean} props.disabled
5339
- * @param {String} props.label
5340
- * @param {Function} props.getValue
5341
- * @param {Function} props.setValue
5342
- * @param {Function} props.onFocus
5343
- * @param {Function} props.onBlur
5344
- * @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
5345
5996
  */
5346
5997
  function TextfieldEntry(props) {
5347
5998
  const {
@@ -5608,7 +6259,7 @@ function AltTextEntry(props) {
5608
6259
  component: AltText,
5609
6260
  editField: editField,
5610
6261
  field: field,
5611
- isEdited: isEdited$6
6262
+ isEdited: isEdited$7
5612
6263
  });
5613
6264
  }
5614
6265
  return entries;
@@ -5642,59 +6293,70 @@ function AltText(props) {
5642
6293
  });
5643
6294
  }
5644
6295
 
6296
+ const AUTO_OPTION_VALUE = '';
5645
6297
  function ColumnsEntry(props) {
5646
6298
  const {
5647
6299
  editField,
5648
6300
  field
5649
6301
  } = props;
5650
- const {
5651
- type
5652
- } = field;
5653
- const entries = [];
5654
- if (type === 'columns') {
5655
- entries.push({
5656
- id: 'columns',
5657
- component: Columns,
5658
- editField: editField,
5659
- field: field,
5660
- isEdited: isEdited$5
5661
- });
5662
- }
6302
+ const entries = [{
6303
+ id: 'columns',
6304
+ component: Columns,
6305
+ field,
6306
+ editField,
6307
+ isEdited: isEdited$4
6308
+ }];
5663
6309
  return entries;
5664
6310
  }
5665
6311
  function Columns(props) {
5666
6312
  const {
5667
- editField,
5668
6313
  field,
6314
+ editField,
5669
6315
  id
5670
6316
  } = props;
5671
6317
  const debounce = useService('debounce');
5672
- const getValue = () => {
5673
- return field.components.length;
6318
+ const formLayoutValidator = useService('formLayoutValidator');
6319
+ const validate = value => {
6320
+ return formLayoutValidator.validateField(field, value ? parseInt(value) : null);
5674
6321
  };
5675
6322
  const setValue = value => {
5676
- let components = field.components.slice();
5677
- if (value > components.length) {
5678
- while (value > components.length) {
5679
- components.push(Default.create({
5680
- _parent: field.id
5681
- }));
5682
- }
5683
- } else {
5684
- components = components.slice(0, value);
5685
- }
5686
- 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));
5687
6326
  };
5688
- 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({
5689
6340
  debounce,
5690
6341
  element: field,
5691
- getValue,
5692
6342
  id,
5693
6343
  label: 'Columns',
5694
- setValue
6344
+ getOptions,
6345
+ getValue,
6346
+ setValue,
6347
+ validate
5695
6348
  });
5696
6349
  }
5697
6350
 
6351
+ // helper /////////
6352
+
6353
+ function asOption(number) {
6354
+ return {
6355
+ value: number,
6356
+ label: number.toString()
6357
+ };
6358
+ }
6359
+
5698
6360
  function DescriptionEntry(props) {
5699
6361
  const {
5700
6362
  editField,
@@ -5989,7 +6651,7 @@ function DisabledEntry(props) {
5989
6651
  component: Disabled,
5990
6652
  editField: editField,
5991
6653
  field: field,
5992
- isEdited: isEdited$7
6654
+ isEdited: isEdited$8
5993
6655
  });
5994
6656
  }
5995
6657
  return entries;
@@ -6199,6 +6861,7 @@ function simpleBoolEntryFactory(options) {
6199
6861
  const {
6200
6862
  id,
6201
6863
  label,
6864
+ description,
6202
6865
  path,
6203
6866
  props
6204
6867
  } = options;
@@ -6212,8 +6875,9 @@ function simpleBoolEntryFactory(options) {
6212
6875
  path,
6213
6876
  field,
6214
6877
  editField,
6878
+ description,
6215
6879
  component: SimpleBoolComponent,
6216
- isEdited: isEdited$7
6880
+ isEdited: isEdited$8
6217
6881
  };
6218
6882
  }
6219
6883
  const SimpleBoolComponent = props => {
@@ -6222,7 +6886,8 @@ const SimpleBoolComponent = props => {
6222
6886
  label,
6223
6887
  path,
6224
6888
  field,
6225
- editField
6889
+ editField,
6890
+ description
6226
6891
  } = props;
6227
6892
  const getValue = () => get(field, path, '');
6228
6893
  const setValue = value => editField(field, path, value);
@@ -6231,7 +6896,8 @@ const SimpleBoolComponent = props => {
6231
6896
  getValue,
6232
6897
  id,
6233
6898
  label,
6234
- setValue
6899
+ setValue,
6900
+ description
6235
6901
  });
6236
6902
  };
6237
6903
 
@@ -6287,7 +6953,7 @@ function SourceEntry(props) {
6287
6953
  component: Source,
6288
6954
  editField: editField,
6289
6955
  field: field,
6290
- isEdited: isEdited$6
6956
+ isEdited: isEdited$7
6291
6957
  });
6292
6958
  }
6293
6959
  return entries;
@@ -6325,21 +6991,38 @@ function Source(props) {
6325
6991
  function TextEntry(props) {
6326
6992
  const {
6327
6993
  editField,
6994
+ /* getService, */
6328
6995
  field
6329
6996
  } = props;
6330
6997
  const {
6331
6998
  type
6332
6999
  } = field;
7000
+
7001
+ // const templating = getService('templating');
7002
+
6333
7003
  if (type !== 'text') {
6334
7004
  return [];
6335
7005
  }
6336
- return [{
7006
+ const entries = [{
6337
7007
  id: 'text',
6338
7008
  component: Text,
6339
7009
  editField: editField,
6340
7010
  field: field,
6341
- isEdited: isEdited$6
7011
+ isEdited: isEdited$7
6342
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;
6343
7026
  }
6344
7027
  function Text(props) {
6345
7028
  const {
@@ -6358,15 +7041,21 @@ function Text(props) {
6358
7041
  const setValue = value => {
6359
7042
  return editField(field, path, value);
6360
7043
  };
6361
- 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({
6362
7052
  debounce,
6363
- description: 'Use an Expression, Markdown or basic HTML to format.',
7053
+ description,
6364
7054
  element: field,
6365
- feel: 'optional',
6366
7055
  getValue,
6367
7056
  id,
6368
7057
  label: 'Text',
6369
- rows: 10,
7058
+ hostLanguage: 'markdown',
6370
7059
  setValue,
6371
7060
  variables
6372
7061
  });
@@ -6482,7 +7171,7 @@ function NumberSerializationEntry(props) {
6482
7171
  entries.push({
6483
7172
  id: 'serialize-to-string',
6484
7173
  component: SerializeToString,
6485
- isEdited: isEdited$7,
7174
+ isEdited: isEdited$8,
6486
7175
  editField,
6487
7176
  field
6488
7177
  });
@@ -6541,7 +7230,7 @@ function DateTimeEntry(props) {
6541
7230
  entries.push({
6542
7231
  id: 'use24h',
6543
7232
  component: Use24h,
6544
- isEdited: isEdited$7,
7233
+ isEdited: isEdited$8,
6545
7234
  editField,
6546
7235
  field
6547
7236
  });
@@ -6654,7 +7343,7 @@ function DateTimeConstraintsEntry(props) {
6654
7343
  entries.push({
6655
7344
  id: id + '-disallowPassedDates',
6656
7345
  component: DisallowPassedDates,
6657
- isEdited: isEdited$7,
7346
+ isEdited: isEdited$8,
6658
7347
  editField,
6659
7348
  field
6660
7349
  });
@@ -7029,11 +7718,19 @@ function InputKeyValuesSourceEntry(props) {
7029
7718
  field,
7030
7719
  id
7031
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
+ });
7032
7729
  return [{
7033
7730
  id: id + '-key',
7034
7731
  component: InputValuesKey,
7035
7732
  label: 'Input values key',
7036
- description: 'Define which input property to populate the values from',
7733
+ description,
7037
7734
  isEdited: isEdited$1,
7038
7735
  editField,
7039
7736
  field
@@ -7228,7 +7925,7 @@ function ConditionEntry(props) {
7228
7925
  component: Condition,
7229
7926
  editField: editField,
7230
7927
  field: field,
7231
- isEdited: isEdited$6
7928
+ isEdited: isEdited$7
7232
7929
  }];
7233
7930
  }
7234
7931
  function Condition(props) {
@@ -7266,7 +7963,7 @@ function Condition(props) {
7266
7963
  });
7267
7964
  }
7268
7965
 
7269
- function GeneralGroup(field, editField) {
7966
+ function GeneralGroup(field, editField, getService) {
7270
7967
  const entries = [...IdEntry({
7271
7968
  field,
7272
7969
  editField
@@ -7285,15 +7982,13 @@ function GeneralGroup(field, editField) {
7285
7982
  }), ...ActionEntry({
7286
7983
  field,
7287
7984
  editField
7288
- }), ...ColumnsEntry({
7289
- field,
7290
- editField
7291
7985
  }), ...DateTimeEntry({
7292
7986
  field,
7293
7987
  editField
7294
7988
  }), ...TextEntry({
7295
7989
  field,
7296
- editField
7990
+ editField,
7991
+ getService
7297
7992
  }), ...NumberEntries({
7298
7993
  field,
7299
7994
  editField
@@ -7389,7 +8084,7 @@ function ValidationGroup(field, editField) {
7389
8084
  component: Required,
7390
8085
  getValue,
7391
8086
  field,
7392
- isEdited: isEdited$7,
8087
+ isEdited: isEdited$8,
7393
8088
  onChange
7394
8089
  }];
7395
8090
  if (type === 'textfield') {
@@ -7731,6 +8426,27 @@ function AppearanceGroup(field, editField) {
7731
8426
  };
7732
8427
  }
7733
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
+
7734
8450
  function ConditionGroup(field, editField) {
7735
8451
  const {
7736
8452
  type
@@ -7749,11 +8465,11 @@ function ConditionGroup(field, editField) {
7749
8465
  };
7750
8466
  }
7751
8467
 
7752
- function getGroups(field, editField) {
8468
+ function getGroups(field, editField, getService) {
7753
8469
  if (!field) {
7754
8470
  return [];
7755
8471
  }
7756
- 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)];
7757
8473
 
7758
8474
  // contract: if a group returns null, it should not be displayed at all
7759
8475
  return groups.filter(group => group !== null);
@@ -7806,10 +8522,9 @@ function FormPropertiesPanel(props) {
7806
8522
  };
7807
8523
  }, []);
7808
8524
  const selectedFormField = state.selectedFormField;
8525
+ const getService = (type, strict = true) => injector.get(type, strict);
7809
8526
  const propertiesPanelContext = {
7810
- getService(type, strict = true) {
7811
- return injector.get(type, strict);
7812
- }
8527
+ getService
7813
8528
  };
7814
8529
  const onFocus = () => eventBus.fire('propertiesPanel.focusin');
7815
8530
  const onBlur = () => eventBus.fire('propertiesPanel.focusout');
@@ -7824,7 +8539,7 @@ function FormPropertiesPanel(props) {
7824
8539
  children: jsx(PropertiesPanel, {
7825
8540
  element: selectedFormField,
7826
8541
  eventBus: eventBus,
7827
- groups: getGroups(selectedFormField, editField),
8542
+ groups: getGroups(selectedFormField, editField, getService),
7828
8543
  headerProvider: PropertiesPanelHeaderProvider,
7829
8544
  placeholderProvider: PropertiesPanelPlaceholderProvider
7830
8545
  })
@@ -7902,6 +8617,12 @@ var PropertiesPanelModule = {
7902
8617
  propertiesPanel: ['type', PropertiesPanelRenderer]
7903
8618
  };
7904
8619
 
8620
+ var ExpressionLanguageModule = {
8621
+ __init__: ['expressionLanguage', 'templating'],
8622
+ expressionLanguage: ['type', FeelExpressionLanguage],
8623
+ templating: ['type', FeelersTemplating]
8624
+ };
8625
+
7905
8626
  const ids = new Ids([32, 36, 1]);
7906
8627
 
7907
8628
  /**
@@ -8154,7 +8875,7 @@ class FormEditor {
8154
8875
  * @internal
8155
8876
  */
8156
8877
  _getModules() {
8157
- return [ModelingModule, EditorActionsModule, KeyboardModule, SelectionModule, PaletteModule, PropertiesPanelModule];
8878
+ return [ModelingModule, EditorActionsModule, DraggingModule, KeyboardModule, SelectionModule, PaletteModule, ExpressionLanguageModule, MarkdownModule, PropertiesPanelModule];
8158
8879
  }
8159
8880
 
8160
8881
  /**