@bpmn-io/form-js-editor 1.2.0 → 1.3.0-alpha.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 (28) hide show
  1. package/dist/assets/form-js-editor-base.css +50 -11
  2. package/dist/assets/form-js-editor.css +289 -10
  3. package/dist/assets/properties-panel.css +245 -1
  4. package/dist/index.cjs +2320 -1640
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +2320 -1641
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/types/FormEditor.d.ts +1 -0
  9. package/dist/types/core/index.d.ts +7 -5
  10. package/dist/types/features/dragging/Dragging.d.ts +3 -1
  11. package/dist/types/features/modeling/Modeling.d.ts +4 -0
  12. package/dist/types/features/modeling/behavior/KeyBehavior.d.ts +1 -1
  13. package/dist/types/features/modeling/behavior/PathBehavior.d.ts +8 -0
  14. package/dist/types/features/modeling/behavior/index.d.ts +2 -0
  15. package/dist/types/features/modeling/cmd/MoveFormFieldHandler.d.ts +3 -1
  16. package/dist/types/features/modeling/cmd/UpdateKeyClaimHandler.d.ts +3 -3
  17. package/dist/types/features/modeling/cmd/UpdatePathClaimHandler.d.ts +14 -0
  18. package/dist/types/features/modeling/cmd/Util.d.ts +1 -0
  19. package/dist/types/features/modeling/index.d.ts +1 -0
  20. package/dist/types/features/properties-panel/Util.d.ts +1 -0
  21. package/dist/types/features/properties-panel/entries/GroupEntries.d.ts +10 -0
  22. package/dist/types/features/properties-panel/entries/PathEntry.d.ts +9 -0
  23. package/dist/types/features/properties-panel/entries/index.d.ts +2 -0
  24. package/dist/types/render/components/Util.d.ts +1 -2
  25. package/package.json +4 -4
  26. package/dist/types/core/FieldFactory.d.ts +0 -18
  27. package/dist/types/import/Importer.d.ts +0 -53
  28. package/dist/types/import/index.d.ts +0 -5
package/dist/index.cjs CHANGED
@@ -13,6 +13,8 @@ var minDom = require('min-dom');
13
13
  var arrayMove = require('array-move');
14
14
  var feelers = require('feelers');
15
15
  var FeelEditor = require('@bpmn-io/feel-editor');
16
+ var view = require('@codemirror/view');
17
+ var focusTrap = require('focus-trap');
16
18
  var Big = require('big.js');
17
19
 
18
20
  function _interopNamespaceDefault(e) {
@@ -33,6 +35,7 @@ function _interopNamespaceDefault(e) {
33
35
  }
34
36
 
35
37
  var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
38
+ var focusTrap__namespace = /*#__PURE__*/_interopNamespaceDefault(focusTrap);
36
39
 
37
40
  var FN_REF = '__fn';
38
41
  var DEFAULT_PRIORITY$2 = 1000;
@@ -549,73 +552,6 @@ function DebounceFactory(config = true) {
549
552
  }
550
553
  DebounceFactory.$inject = ['config.debounce'];
551
554
 
552
- class FieldFactory {
553
- /**
554
- * @constructor
555
- *
556
- * @param { import('./FormFieldRegistry').default } formFieldRegistry
557
- * @param { import('@bpmn-io/form-js-viewer').FormFields } formFields
558
- */
559
- constructor(formFieldRegistry, formFields) {
560
- this._formFieldRegistry = formFieldRegistry;
561
- this._formFields = formFields;
562
- }
563
- create(attrs, applyDefaults = true) {
564
- const {
565
- id,
566
- key,
567
- type
568
- } = attrs;
569
- const fieldDefinition = this._formFields.get(type);
570
- if (!fieldDefinition) {
571
- throw new Error(`form field of type <${type}> not supported`);
572
- }
573
- const {
574
- config
575
- } = fieldDefinition;
576
- if (id && this._formFieldRegistry._ids.assigned(id)) {
577
- throw new Error(`ID <${id}> already assigned`);
578
- }
579
- if (key && this._formFieldRegistry._keys.assigned(key)) {
580
- throw new Error(`key <${key}> already assigned`);
581
- }
582
- const labelAttrs = applyDefaults && config.label ? {
583
- label: config.label
584
- } : {};
585
- const field = config.create({
586
- ...labelAttrs,
587
- ...attrs
588
- });
589
- this._ensureId(field);
590
- if (config.keyed) {
591
- this._ensureKey(field, applyDefaults);
592
- }
593
- return field;
594
- }
595
- _ensureId(field) {
596
- if (field.id) {
597
- this._formFieldRegistry._ids.claim(field.id, field);
598
- return;
599
- }
600
- let prefix = 'Field';
601
- if (field.type === 'default') {
602
- prefix = 'Form';
603
- }
604
- field.id = this._formFieldRegistry._ids.nextPrefixed(`${prefix}_`, field);
605
- }
606
- _ensureKey(field, applyDefaults) {
607
- if (field.key) {
608
- this._formFieldRegistry._keys.claim(field.key, field);
609
- return;
610
- }
611
- if (applyDefaults) {
612
- let prefix = 'field';
613
- field.key = this._formFieldRegistry._keys.nextPrefixed(`${prefix}_`, field);
614
- }
615
- }
616
- }
617
- FieldFactory.$inject = ['formFieldRegistry', 'formFields'];
618
-
619
555
  class FormFieldRegistry extends formJsViewer.FormFieldRegistry {
620
556
  /**
621
557
  * Updates a form fields id.
@@ -728,111 +664,7 @@ function calculateMaxColumnsWithAuto(autoCols) {
728
664
  return MAX_COLUMNS_PER_ROW - autoCols * 2;
729
665
  }
730
666
 
731
- class Importer {
732
- /**
733
- * @constructor
734
- * @param { import('../core/FormFieldRegistry').default } formFieldRegistry
735
- * @param { import('../core/FieldFactory').default } fieldFactory
736
- * @param { import('../core/FormLayouter').default } formLayouter
737
- */
738
- constructor(formFieldRegistry, fieldFactory, formLayouter) {
739
- this._formFieldRegistry = formFieldRegistry;
740
- this._fieldFactory = fieldFactory;
741
- this._formLayouter = formLayouter;
742
- }
743
-
744
- /**
745
- * Import schema creating rows, fields, attaching additional
746
- * information to each field and adding fields to the
747
- * field registry.
748
- *
749
- * Additional information attached:
750
- *
751
- * * `id` (unless present)
752
- * * `_parent`
753
- * * `_path`
754
- *
755
- * @param {any} schema
756
- *
757
- * @typedef {{ warnings: Error[], schema: any }} ImportResult
758
- * @returns {ImportResult}
759
- */
760
- importSchema(schema) {
761
- // TODO: Add warnings
762
- const warnings = [];
763
- try {
764
- const importedSchema = this.importFormField(formJsViewer.clone(schema));
765
- this._formLayouter.calculateLayout(formJsViewer.clone(importedSchema));
766
- return {
767
- schema: importedSchema,
768
- warnings
769
- };
770
- } catch (err) {
771
- err.warnings = warnings;
772
- throw err;
773
- }
774
- }
775
-
776
- /**
777
- * @param {{[x: string]: any}} fieldAttrs
778
- * @param {String} [parentId]
779
- * @param {number} [index]
780
- *
781
- * @return {any} field
782
- */
783
- importFormField(fieldAttrs, parentId, index) {
784
- const {
785
- components,
786
- id,
787
- key
788
- } = fieldAttrs;
789
- let parent, path;
790
- if (parentId) {
791
- parent = this._formFieldRegistry.get(parentId);
792
- }
793
-
794
- // validate <id> uniqueness
795
- if (id && this._formFieldRegistry._ids.assigned(id)) {
796
- throw new Error(`form field with id <${id}> already exists`);
797
- }
798
-
799
- // validate <key> uniqueness
800
- if (key && this._formFieldRegistry._keys.assigned(key)) {
801
- throw new Error(`form field with key <${key}> already exists`);
802
- }
803
-
804
- // set form field path
805
- path = parent ? [...parent._path, 'components', index] : [];
806
- const field = this._fieldFactory.create({
807
- ...fieldAttrs,
808
- _path: path,
809
- _parent: parent && parent.id
810
- }, false);
811
- this._formFieldRegistry.add(field);
812
- if (components) {
813
- field.components = this.importFormFields(components, field.id);
814
- }
815
- return field;
816
- }
817
-
818
- /**
819
- * @param {Array<any>} components
820
- * @param {string} parentId
821
- *
822
- * @return {Array<any>} imported components
823
- */
824
- importFormFields(components, parentId) {
825
- return components.map((component, index) => {
826
- return this.importFormField(component, parentId, index);
827
- });
828
- }
829
- }
830
- Importer.$inject = ['formFieldRegistry', 'fieldFactory', 'formLayouter'];
831
-
832
- var importModule = {
833
- importer: ['type', Importer]
834
- };
835
-
667
+ const emptyImage = createEmptyImage();
836
668
  function editorFormFieldClasses(type, {
837
669
  disabled = false
838
670
  } = {}) {
@@ -857,11 +689,10 @@ function editorFormFieldClasses(type, {
857
689
  * domElement.addEventListener('dragstart', dragger(dragMove));
858
690
  *
859
691
  * @param {Function} fn
860
- * @param {Element} dragPreview
861
692
  *
862
693
  * @return {Function} drag start callback function
863
694
  */
864
- function createDragger(fn, dragPreview) {
695
+ function createDragger$1(fn) {
865
696
  let self;
866
697
  let startX, startY;
867
698
 
@@ -871,9 +702,9 @@ function createDragger(fn, dragPreview) {
871
702
  startX = event.clientX;
872
703
  startY = event.clientY;
873
704
 
874
- // (1) prevent preview image
705
+ // (1) hide drag preview image
875
706
  if (event.dataTransfer) {
876
- event.dataTransfer.setDragImage(dragPreview, 0, 0);
707
+ event.dataTransfer.setDragImage(emptyImage, 0, 0);
877
708
  }
878
709
 
879
710
  // (2) setup drag listeners
@@ -881,7 +712,7 @@ function createDragger(fn, dragPreview) {
881
712
  // attach drag + cleanup event
882
713
  document.addEventListener('dragover', onDrag);
883
714
  document.addEventListener('dragend', onEnd);
884
- document.addEventListener('drop', preventDefault);
715
+ document.addEventListener('drop', preventDefault$1);
885
716
  }
886
717
  function onDrag(event) {
887
718
  const delta = {
@@ -895,7 +726,7 @@ function createDragger(fn, dragPreview) {
895
726
  function onEnd() {
896
727
  document.removeEventListener('dragover', onDrag);
897
728
  document.removeEventListener('dragend', onEnd);
898
- document.removeEventListener('drop', preventDefault);
729
+ document.removeEventListener('drop', preventDefault$1);
899
730
  }
900
731
  return onDragStart;
901
732
  }
@@ -924,10 +755,15 @@ function throttle(fn) {
924
755
  });
925
756
  };
926
757
  }
927
- function preventDefault(event) {
758
+ function preventDefault$1(event) {
928
759
  event.preventDefault();
929
760
  event.stopPropagation();
930
761
  }
762
+ function createEmptyImage() {
763
+ const img = new Image();
764
+ img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
765
+ return img;
766
+ }
931
767
 
932
768
  const DragAndDropContext = preact.createContext({
933
769
  drake: null
@@ -1585,13 +1421,15 @@ class Dragging {
1585
1421
  * @param { import('../../core/FormLayoutValidator').default } formLayoutValidator
1586
1422
  * @param { import('../../core/EventBus').default } eventBus
1587
1423
  * @param { import('../modeling/Modeling').default } modeling
1424
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
1588
1425
  */
1589
- constructor(formFieldRegistry, formLayouter, formLayoutValidator, eventBus, modeling) {
1426
+ constructor(formFieldRegistry, formLayouter, formLayoutValidator, eventBus, modeling, pathRegistry) {
1590
1427
  this._formFieldRegistry = formFieldRegistry;
1591
1428
  this._formLayouter = formLayouter;
1592
1429
  this._formLayoutValidator = formLayoutValidator;
1593
1430
  this._eventBus = eventBus;
1594
1431
  this._modeling = modeling;
1432
+ this._pathRegistry = pathRegistry;
1595
1433
  }
1596
1434
 
1597
1435
  /**
@@ -1625,11 +1463,50 @@ class Dragging {
1625
1463
  const targetRow = this._formLayouter.getRow(target.dataset.rowId);
1626
1464
  let columns;
1627
1465
  let formField;
1466
+ let targetParentId;
1628
1467
  if (formFieldNode) {
1629
1468
  formField = this._formFieldRegistry.get(formFieldNode.dataset.id);
1630
1469
  columns = (formField.layout || {}).columns;
1470
+
1471
+ // (1) check for row constraints
1472
+ if (isRow(target)) {
1473
+ targetParentId = getFormParent(target).dataset.id;
1474
+ const rowError = this._formLayoutValidator.validateField(formField, columns, targetRow);
1475
+ if (rowError) {
1476
+ return rowError;
1477
+ }
1478
+ } else {
1479
+ targetParentId = target.dataset.id;
1480
+ }
1481
+
1482
+ // (2) check target is a valid parent
1483
+ if (!targetParentId) {
1484
+ return 'Drop is not a valid target';
1485
+ }
1486
+
1487
+ // (3) check for path collisions
1488
+ const targetParentFormField = this._formFieldRegistry.get(targetParentId);
1489
+ const currentParentFormField = this._formFieldRegistry.get(formField._parent);
1490
+ if (targetParentFormField !== currentParentFormField) {
1491
+ const targetParentPath = this._pathRegistry.getValuePath(targetParentFormField);
1492
+ const currentParentPath = this._pathRegistry.getValuePath(currentParentFormField);
1493
+ if (targetParentPath.join('.') !== currentParentPath.join('.')) {
1494
+ const isDropAllowedByPathRegistry = this._pathRegistry.executeRecursivelyOnFields(formField, ({
1495
+ field,
1496
+ isClosed
1497
+ }) => {
1498
+ const options = {
1499
+ cutoffNode: currentParentFormField.id
1500
+ };
1501
+ const fieldPath = this._pathRegistry.getValuePath(field, options);
1502
+ return this._pathRegistry.canClaimPath([...targetParentPath, ...fieldPath], isClosed);
1503
+ });
1504
+ if (!isDropAllowedByPathRegistry) {
1505
+ return 'Drop not allowed by path registry';
1506
+ }
1507
+ }
1508
+ }
1631
1509
  }
1632
- return this._formLayoutValidator.validateField(formField, columns, targetRow);
1633
1510
  }
1634
1511
  moveField(element, source, targetRow, targetFormField, targetIndex) {
1635
1512
  const formFieldNode = element.querySelector('.fjs-element');
@@ -1647,6 +1524,7 @@ class Dragging {
1647
1524
  };
1648
1525
  attrs = {
1649
1526
  ...attrs,
1527
+ _parent: targetFormField.id,
1650
1528
  layout: {
1651
1529
  row: targetRow ? targetRow.id : this._formLayouter.nextRowId(),
1652
1530
  // enable auto columns
@@ -1680,14 +1558,13 @@ class Dragging {
1680
1558
 
1681
1559
  // (2.1) dropped in existing row
1682
1560
  if (isRow(target)) {
1683
- unsetDropNotAllowed(target);
1684
1561
  targetRow = this._formLayouter.getRow(target.dataset.rowId);
1562
+ }
1685
1563
 
1686
- // validate whether drop is allowed
1687
- const validationError = this.validateDrop(el, target);
1688
- if (validationError) {
1689
- return drake.cancel(true);
1690
- }
1564
+ // (2.2) validate whether drop is allowed
1565
+ const validationError = this.validateDrop(el, target);
1566
+ if (validationError) {
1567
+ return drake.cancel(true);
1691
1568
  }
1692
1569
  drake.remove();
1693
1570
 
@@ -1735,13 +1612,11 @@ class Dragging {
1735
1612
  return !target.classList.contains(DROP_CONTAINER_HORIZONTAL_CLS);
1736
1613
  }
1737
1614
 
1738
- // validate field drop in row
1739
- if (isRow(target)) {
1740
- const validationError = this.validateDrop(el, target);
1741
- if (validationError) {
1742
- // set error feedback to row
1743
- setDropNotAllowed(target);
1744
- }
1615
+ // validate field drop
1616
+ const validationError = this.validateDrop(el, target);
1617
+ if (validationError) {
1618
+ // set error feedback to row
1619
+ setDropNotAllowed(target);
1745
1620
  }
1746
1621
  return !target.classList.contains(DRAG_NO_DROP_CLS);
1747
1622
  },
@@ -1812,7 +1687,7 @@ class Dragging {
1812
1687
  this._eventBus.fire(event, context);
1813
1688
  }
1814
1689
  }
1815
- Dragging.$inject = ['formFieldRegistry', 'formLayouter', 'formLayoutValidator', 'eventBus', 'modeling'];
1690
+ Dragging.$inject = ['formFieldRegistry', 'formLayouter', 'formLayoutValidator', 'eventBus', 'modeling', 'pathRegistry'];
1816
1691
 
1817
1692
  // helper //////////
1818
1693
 
@@ -1870,7 +1745,6 @@ function FieldDragPreview(props) {
1870
1745
 
1871
1746
  const COLUMNS_REGEX = /^cds--col(-lg)?/;
1872
1747
  const ELEMENT_RESIZING_CLS = 'fjs-element-resizing';
1873
- const RESIZE_DRAG_PREVIEW_CLS = 'fjs-resize-drag-preview';
1874
1748
  const GRID_OFFSET_PX = 16;
1875
1749
  function FieldResizer(props) {
1876
1750
  const {
@@ -1909,17 +1783,8 @@ function FieldResizer(props) {
1909
1783
  const target = getElementNode(field);
1910
1784
  const parent = getParent(target);
1911
1785
 
1912
- // create a blank element to use as drag preview
1913
- // ensure it was only created once
1914
- let blankPreview = getDragPreviewImage(parent);
1915
- if (!blankPreview) {
1916
- blankPreview = document.createElement('div');
1917
- blankPreview.classList.add(RESIZE_DRAG_PREVIEW_CLS);
1918
- parent.appendChild(blankPreview);
1919
- }
1920
-
1921
1786
  // initialize drag handler
1922
- const onDragStart = createDragger(onResize, blankPreview);
1787
+ const onDragStart = createDragger$1(onResize);
1923
1788
  onDragStart(event);
1924
1789
 
1925
1790
  // mitigate auto columns on the grid that
@@ -1942,10 +1807,6 @@ function FieldResizer(props) {
1942
1807
  const target = getElementNode(field);
1943
1808
  unsetResizing(target, position);
1944
1809
  context.current.newColumns = null;
1945
-
1946
- // remove blank preview
1947
- const blankPreview = getDragPreviewImage(getParent(target));
1948
- blankPreview.remove();
1949
1810
  };
1950
1811
  if (field.type === 'default') {
1951
1812
  return null;
@@ -1988,9 +1849,6 @@ function getColumnNode(node) {
1988
1849
  function getElementNode(field) {
1989
1850
  return minDom.query('.fjs-element[data-id="' + field.id + '"]');
1990
1851
  }
1991
- function getDragPreviewImage(node) {
1992
- return minDom.query('.fjs-resize-drag-preview', node);
1993
- }
1994
1852
  function setResizing(node, position) {
1995
1853
  minDom.classes(node).add(ELEMENT_RESIZING_CLS + '-' + position);
1996
1854
  }
@@ -2007,7 +1865,10 @@ function ContextPad(props) {
2007
1865
  children: props.children
2008
1866
  });
2009
1867
  }
2010
- function Empty(props) {
1868
+ function Empty() {
1869
+ return null;
1870
+ }
1871
+ function EmptyRoot(props) {
2011
1872
  return jsxRuntime.jsx("div", {
2012
1873
  class: "fjs-empty-editor",
2013
1874
  children: jsxRuntime.jsxs("div", {
@@ -2028,12 +1889,17 @@ function Element$1(props) {
2028
1889
  formFieldRegistry = useService$1('formFieldRegistry'),
2029
1890
  modeling = useService$1('modeling'),
2030
1891
  selection = useService$1('selection');
1892
+ const {
1893
+ hoveredId,
1894
+ setHoveredId
1895
+ } = hooks.useContext(formJsViewer.FormRenderContext);
2031
1896
  const {
2032
1897
  field
2033
1898
  } = props;
2034
1899
  const {
2035
1900
  id,
2036
- type
1901
+ type,
1902
+ showOutline
2037
1903
  } = field;
2038
1904
  const ref = hooks.useRef();
2039
1905
  function scrollIntoView({
@@ -2071,6 +1937,12 @@ function Element$1(props) {
2071
1937
  if (selection.isSelected(field)) {
2072
1938
  classes.push('fjs-editor-selected');
2073
1939
  }
1940
+ if (showOutline) {
1941
+ classes.push('fjs-outlined');
1942
+ }
1943
+ if (hoveredId === field.id) {
1944
+ classes.push('fjs-editor-hovered');
1945
+ }
2074
1946
  const onRemove = event => {
2075
1947
  event.stopPropagation();
2076
1948
  const parentField = formFieldRegistry.get(field._parent);
@@ -2090,6 +1962,11 @@ function Element$1(props) {
2090
1962
  tabIndex: type === 'default' ? -1 : 0,
2091
1963
  onClick: onClick,
2092
1964
  onKeyPress: onKeyPress,
1965
+ onMouseOver: e => {
1966
+ // @ts-ignore
1967
+ setHoveredId(field.id);
1968
+ e.stopPropagation();
1969
+ },
2093
1970
  ref: ref,
2094
1971
  children: [jsxRuntime.jsx(DebugColumns, {
2095
1972
  field: field
@@ -2259,14 +2136,18 @@ function FormEditor$1(props) {
2259
2136
  hooks.useEffect(() => {
2260
2137
  eventBus.fire('formEditor.rendered');
2261
2138
  }, []);
2262
- const formRenderContext = {
2139
+ const [hoveredId, setHoveredId] = hooks.useState(null);
2140
+ const formRenderContext = hooks.useMemo(() => ({
2263
2141
  Children,
2264
2142
  Column,
2265
2143
  Element: Element$1,
2266
2144
  Empty,
2267
- Row
2268
- };
2269
- const formContext = {
2145
+ EmptyRoot,
2146
+ Row,
2147
+ hoveredId,
2148
+ setHoveredId
2149
+ }), [hoveredId]);
2150
+ const formContext = hooks.useMemo(() => ({
2270
2151
  getService(type, strict = true) {
2271
2152
  // TODO(philippfromme): clean up
2272
2153
  if (type === 'form') {
@@ -2287,7 +2168,7 @@ function FormEditor$1(props) {
2287
2168
  return injector.get(type, strict);
2288
2169
  },
2289
2170
  formId: formEditor._id
2290
- };
2171
+ }), [ariaLabel, formEditor, injector, schema]);
2291
2172
  const onSubmit = hooks.useCallback(() => {}, []);
2292
2173
  const onReset = hooks.useCallback(() => {}, []);
2293
2174
 
@@ -2313,6 +2194,7 @@ function FormEditor$1(props) {
2313
2194
  children: jsxRuntime.jsx(formJsViewer.FormContext.Provider, {
2314
2195
  value: formContext,
2315
2196
  children: jsxRuntime.jsx(formJsViewer.FormRenderContext.Provider, {
2197
+ // @ts-ignore
2316
2198
  value: formRenderContext,
2317
2199
  children: jsxRuntime.jsx(formJsViewer.FormComponent, {
2318
2200
  onSubmit: onSubmit,
@@ -2450,13 +2332,15 @@ var renderModule = {
2450
2332
  };
2451
2333
 
2452
2334
  var core = {
2453
- __depends__: [importModule, renderModule],
2335
+ __depends__: [renderModule],
2454
2336
  debounce: ['factory', DebounceFactory],
2455
2337
  eventBus: ['type', EventBus],
2338
+ importer: ['type', formJsViewer.Importer],
2456
2339
  formFieldRegistry: ['type', FormFieldRegistry],
2340
+ pathRegistry: ['type', formJsViewer.PathRegistry],
2457
2341
  formLayouter: ['type', formJsViewer.FormLayouter],
2458
2342
  formLayoutValidator: ['type', FormLayoutValidator],
2459
- fieldFactory: ['type', FieldFactory]
2343
+ fieldFactory: ['type', formJsViewer.FieldFactory]
2460
2344
  };
2461
2345
 
2462
2346
  /**
@@ -3162,9 +3046,16 @@ function arrayRemove(array, index) {
3162
3046
  }
3163
3047
  function updatePath(formFieldRegistry, formField, index) {
3164
3048
  const parent = formFieldRegistry.get(formField._parent);
3165
- formField._path = [...parent._path, 'components', index];
3049
+ refreshPathsRecursively(formField, [...parent._path, 'components', index]);
3166
3050
  return formField;
3167
3051
  }
3052
+ function refreshPathsRecursively(formField, path) {
3053
+ formField._path = path;
3054
+ const components = formField.components || [];
3055
+ components.forEach((component, index) => {
3056
+ refreshPathsRecursively(component, [...path, 'components', index]);
3057
+ });
3058
+ }
3168
3059
  function updateRow(formField, rowId) {
3169
3060
  formField.layout = {
3170
3061
  ...(formField.layout || {}),
@@ -3198,7 +3089,7 @@ class AddFormFieldHandler {
3198
3089
  // (1) Add new form field
3199
3090
  arrayAdd$1(minDash.get(schema, targetPath), targetIndex, formField);
3200
3091
 
3201
- // (2) Update paths of new form field and its siblings
3092
+ // (2) Update internal paths of new form field and its siblings (and their children)
3202
3093
  minDash.get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3203
3094
 
3204
3095
  // (3) Add new form field to form field registry
@@ -3223,7 +3114,7 @@ class AddFormFieldHandler {
3223
3114
  // (1) Remove new form field
3224
3115
  arrayRemove(minDash.get(schema, targetPath), targetIndex);
3225
3116
 
3226
- // (2) Update paths of new form field and its siblings
3117
+ // (2) Update internal paths of new form field and its siblings (and their children)
3227
3118
  minDash.get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3228
3119
 
3229
3120
  // (3) Remove new form field from form field registry
@@ -3308,10 +3199,12 @@ class MoveFormFieldHandler {
3308
3199
  * @constructor
3309
3200
  * @param { import('../../../FormEditor').default } formEditor
3310
3201
  * @param { import('../../../core/FormFieldRegistry').default } formFieldRegistry
3202
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
3311
3203
  */
3312
- constructor(formEditor, formFieldRegistry) {
3204
+ constructor(formEditor, formFieldRegistry, pathRegistry) {
3313
3205
  this._formEditor = formEditor;
3314
3206
  this._formFieldRegistry = formFieldRegistry;
3207
+ this._pathRegistry = pathRegistry;
3315
3208
  }
3316
3209
  execute(context) {
3317
3210
  this.moveFormField(context);
@@ -3364,27 +3257,42 @@ class MoveFormFieldHandler {
3364
3257
  // (2) Move form field
3365
3258
  arrayMove.mutate(minDash.get(schema, sourcePath), sourceIndex, targetIndex);
3366
3259
 
3367
- // (3) Update paths of new form field and its siblings
3260
+ // (3) Update internal paths of new form field and its siblings (and their children)
3368
3261
  minDash.get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3369
3262
  } else {
3370
3263
  const formField = minDash.get(schema, [...sourcePath, sourceIndex]);
3264
+
3265
+ // (1) Deregister form field (and children) from path registry
3266
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3267
+ field
3268
+ }) => {
3269
+ this._pathRegistry.unclaimPath(this._pathRegistry.getValuePath(field));
3270
+ });
3371
3271
  formField._parent = targetFormField.id;
3372
3272
 
3373
- // (1) Remove form field
3273
+ // (2) Remove form field
3374
3274
  arrayRemove(minDash.get(schema, sourcePath), sourceIndex);
3375
3275
 
3376
- // (2) Update paths of siblings
3276
+ // (3) Update internal paths of siblings (and their children)
3377
3277
  minDash.get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3378
3278
  const targetPath = [...targetFormField._path, 'components'];
3379
3279
 
3380
- // (3) Add to row
3280
+ // (4) Add to row
3381
3281
  updateRow(formField, targetRow ? targetRow.id : null);
3382
3282
 
3383
- // (4) Add form field
3283
+ // (5) Add form field
3384
3284
  arrayAdd$1(minDash.get(schema, targetPath), targetIndex, formField);
3385
3285
 
3386
- // (5) Update paths of siblings
3286
+ // (6) Update internal paths of siblings (and their children)
3387
3287
  minDash.get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3288
+
3289
+ // (7) Reregister form field (and children) from path registry
3290
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3291
+ field,
3292
+ isClosed
3293
+ }) => {
3294
+ this._pathRegistry.claimPath(this._pathRegistry.getValuePath(field), isClosed);
3295
+ });
3388
3296
  }
3389
3297
 
3390
3298
  // TODO: Create updater/change support that automatically updates paths and schema on command execution
@@ -3393,7 +3301,7 @@ class MoveFormFieldHandler {
3393
3301
  });
3394
3302
  }
3395
3303
  }
3396
- MoveFormFieldHandler.$inject = ['formEditor', 'formFieldRegistry'];
3304
+ MoveFormFieldHandler.$inject = ['formEditor', 'formFieldRegistry', 'pathRegistry'];
3397
3305
 
3398
3306
  class RemoveFormFieldHandler {
3399
3307
  /**
@@ -3419,11 +3327,11 @@ class RemoveFormFieldHandler {
3419
3327
  // (1) Remove form field
3420
3328
  arrayRemove(minDash.get(schema, sourcePath), sourceIndex);
3421
3329
 
3422
- // (2) Update paths of its siblings
3330
+ // (2) Update internal paths of its siblings (and their children)
3423
3331
  minDash.get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3424
3332
 
3425
- // (3) Remove form field from form field registry
3426
- this._formFieldRegistry.remove(formField);
3333
+ // (3) Remove form field and children from form field registry
3334
+ formJsViewer.runRecursively(formField, formField => this._formFieldRegistry.remove(formField));
3427
3335
 
3428
3336
  // TODO: Create updater/change support that automatically updates paths and schema on command execution
3429
3337
  this._formEditor._setState({
@@ -3444,11 +3352,11 @@ class RemoveFormFieldHandler {
3444
3352
  // (1) Add form field
3445
3353
  arrayAdd$1(minDash.get(schema, sourcePath), sourceIndex, formField);
3446
3354
 
3447
- // (2) Update paths of its siblings
3355
+ // (2) Update internal paths of its siblings (and their children)
3448
3356
  minDash.get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3449
3357
 
3450
- // (3) Add form field to form field registry
3451
- this._formFieldRegistry.add(formField);
3358
+ // (3) Add form field and children to form field registry
3359
+ formJsViewer.runRecursively(formField, formField => this._formFieldRegistry.add(formField));
3452
3360
 
3453
3361
  // TODO: Create updater/change support that automatically updates paths and schema on command execution
3454
3362
  this._formEditor._setState({
@@ -3496,10 +3404,10 @@ UpdateIdClaimHandler.$inject = ['formFieldRegistry'];
3496
3404
  class UpdateKeyClaimHandler {
3497
3405
  /**
3498
3406
  * @constructor
3499
- * @param { import('../../../core/FormFieldRegistry').default } formFieldRegistry
3407
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
3500
3408
  */
3501
- constructor(formFieldRegistry) {
3502
- this._formFieldRegistry = formFieldRegistry;
3409
+ constructor(pathRegistry) {
3410
+ this._pathRegistry = pathRegistry;
3503
3411
  }
3504
3412
  execute(context) {
3505
3413
  const {
@@ -3507,26 +3415,106 @@ class UpdateKeyClaimHandler {
3507
3415
  formField,
3508
3416
  key
3509
3417
  } = context;
3418
+ const options = {
3419
+ replacements: {
3420
+ [formField.id]: key
3421
+ }
3422
+ };
3423
+ const valuePath = this._pathRegistry.getValuePath(formField, options);
3510
3424
  if (claiming) {
3511
- this._formFieldRegistry._keys.claim(key, formField);
3425
+ this._pathRegistry.claimPath(valuePath, true);
3512
3426
  } else {
3513
- this._formFieldRegistry._keys.unclaim(key);
3427
+ this._pathRegistry.unclaimPath(valuePath);
3514
3428
  }
3429
+
3430
+ // cache path for revert
3431
+ context.valuePath = valuePath;
3515
3432
  }
3516
3433
  revert(context) {
3434
+ const {
3435
+ claiming,
3436
+ valuePath
3437
+ } = context;
3438
+ if (claiming) {
3439
+ this._pathRegistry.unclaimPath(valuePath);
3440
+ } else {
3441
+ this._pathRegistry.claimPath(valuePath, true);
3442
+ }
3443
+ }
3444
+ }
3445
+ UpdateKeyClaimHandler.$inject = ['pathRegistry'];
3446
+
3447
+ class UpdatePathClaimHandler {
3448
+ /**
3449
+ * @constructor
3450
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
3451
+ */
3452
+ constructor(pathRegistry) {
3453
+ this._pathRegistry = pathRegistry;
3454
+ }
3455
+ execute(context) {
3517
3456
  const {
3518
3457
  claiming,
3519
3458
  formField,
3520
- key
3459
+ path
3460
+ } = context;
3461
+ const options = {
3462
+ replacements: {
3463
+ [formField.id]: path
3464
+ }
3465
+ };
3466
+ const valuePaths = [];
3467
+ if (claiming) {
3468
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3469
+ field,
3470
+ isClosed
3471
+ }) => {
3472
+ const valuePath = this._pathRegistry.getValuePath(field, options);
3473
+ valuePaths.push({
3474
+ valuePath,
3475
+ isClosed
3476
+ });
3477
+ this._pathRegistry.claimPath(valuePath, isClosed);
3478
+ });
3479
+ } else {
3480
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3481
+ field,
3482
+ isClosed
3483
+ }) => {
3484
+ const valuePath = this._pathRegistry.getValuePath(field, options);
3485
+ valuePaths.push({
3486
+ valuePath,
3487
+ isClosed
3488
+ });
3489
+ this._pathRegistry.unclaimPath(valuePath);
3490
+ });
3491
+ }
3492
+
3493
+ // cache path info for revert
3494
+ context.valuePaths = valuePaths;
3495
+ }
3496
+ revert(context) {
3497
+ const {
3498
+ claiming,
3499
+ valuePaths
3521
3500
  } = context;
3522
3501
  if (claiming) {
3523
- this._formFieldRegistry._keys.unclaim(key);
3502
+ valuePaths.forEach(({
3503
+ valuePath
3504
+ }) => {
3505
+ this._pathRegistry.unclaimPath(valuePath);
3506
+ });
3524
3507
  } else {
3525
- this._formFieldRegistry._keys.claim(key, formField);
3508
+ valuePaths.forEach(({
3509
+ valuePath,
3510
+ isClosed
3511
+ }) => {
3512
+ this._pathRegistry.claimPath(valuePath, isClosed);
3513
+ });
3526
3514
  }
3527
3515
  }
3528
3516
  }
3529
- UpdateKeyClaimHandler.$inject = ['formFieldRegistry'];
3517
+ UpdatePathClaimHandler.$inject = ['pathRegistry'];
3530
3518
 
3531
3519
  class Modeling {
3532
3520
  constructor(commandStack, eventBus, formEditor, formFieldRegistry, fieldFactory) {
@@ -3550,7 +3538,8 @@ class Modeling {
3550
3538
  'formField.move': MoveFormFieldHandler,
3551
3539
  'formField.remove': RemoveFormFieldHandler,
3552
3540
  'id.updateClaim': UpdateIdClaimHandler,
3553
- 'key.updateClaim': UpdateKeyClaimHandler
3541
+ 'key.updateClaim': UpdateKeyClaimHandler,
3542
+ 'path.updateClaim': UpdatePathClaimHandler
3554
3543
  };
3555
3544
  }
3556
3545
  addFormField(attrs, targetFormField, targetIndex) {
@@ -3627,6 +3616,22 @@ class Modeling {
3627
3616
  };
3628
3617
  this._commandStack.execute('key.updateClaim', context);
3629
3618
  }
3619
+ claimPath(formField, path) {
3620
+ const context = {
3621
+ formField,
3622
+ path,
3623
+ claiming: true
3624
+ };
3625
+ this._commandStack.execute('path.updateClaim', context);
3626
+ }
3627
+ unclaimPath(formField, path) {
3628
+ const context = {
3629
+ formField,
3630
+ path,
3631
+ claiming: false
3632
+ };
3633
+ this._commandStack.execute('path.updateClaim', context);
3634
+ }
3630
3635
  }
3631
3636
  Modeling.$inject = ['commandStack', 'eventBus', 'formEditor', 'formFieldRegistry', 'fieldFactory'];
3632
3637
 
@@ -3899,8 +3904,6 @@ FormLayoutUpdater.$inject = ['eventBus', 'formLayouter', 'modeling', 'formEditor
3899
3904
  class IdBehavior extends CommandInterceptor {
3900
3905
  constructor(eventBus, modeling) {
3901
3906
  super(eventBus);
3902
-
3903
- // @ts-ignore-next-line
3904
3907
  this.preExecute('formField.remove', function (context) {
3905
3908
  const {
3906
3909
  formField
@@ -3910,8 +3913,6 @@ class IdBehavior extends CommandInterceptor {
3910
3913
  } = formField;
3911
3914
  modeling.unclaimId(formField, id);
3912
3915
  }, true);
3913
-
3914
- // @ts-ignore-next-line
3915
3916
  this.preExecute('formField.edit', function (context) {
3916
3917
  const {
3917
3918
  formField,
@@ -3927,36 +3928,82 @@ class IdBehavior extends CommandInterceptor {
3927
3928
  IdBehavior.$inject = ['eventBus', 'modeling'];
3928
3929
 
3929
3930
  class KeyBehavior extends CommandInterceptor {
3930
- constructor(eventBus, modeling) {
3931
+ constructor(eventBus, modeling, formFields) {
3931
3932
  super(eventBus);
3932
-
3933
- // @ts-ignore-next-line
3934
3933
  this.preExecute('formField.remove', function (context) {
3935
3934
  const {
3936
3935
  formField
3937
3936
  } = context;
3938
3937
  const {
3939
- key
3938
+ key,
3939
+ type
3940
3940
  } = formField;
3941
- if (key) {
3941
+ const {
3942
+ config
3943
+ } = formFields.get(type);
3944
+ if (config.keyed) {
3942
3945
  modeling.unclaimKey(formField, key);
3943
3946
  }
3944
3947
  }, true);
3945
-
3946
- // @ts-ignore-next-line
3947
3948
  this.preExecute('formField.edit', function (context) {
3948
3949
  const {
3949
3950
  formField,
3950
3951
  properties
3951
3952
  } = context;
3952
- if ('key' in properties) {
3953
- modeling.unclaimKey(formField, formField.key);
3953
+ const {
3954
+ key,
3955
+ type
3956
+ } = formField;
3957
+ const {
3958
+ config
3959
+ } = formFields.get(type);
3960
+ if (config.keyed && 'key' in properties) {
3961
+ modeling.unclaimKey(formField, key);
3954
3962
  modeling.claimKey(formField, properties.key);
3955
3963
  }
3956
3964
  }, true);
3957
3965
  }
3958
3966
  }
3959
- KeyBehavior.$inject = ['eventBus', 'modeling'];
3967
+ KeyBehavior.$inject = ['eventBus', 'modeling', 'formFields'];
3968
+
3969
+ class PathBehavior extends CommandInterceptor {
3970
+ constructor(eventBus, modeling, formFields) {
3971
+ super(eventBus);
3972
+ this.preExecute('formField.remove', function (context) {
3973
+ const {
3974
+ formField
3975
+ } = context;
3976
+ const {
3977
+ path,
3978
+ type
3979
+ } = formField;
3980
+ const {
3981
+ config
3982
+ } = formFields.get(type);
3983
+ if (config.pathed) {
3984
+ modeling.unclaimPath(formField, path);
3985
+ }
3986
+ }, true);
3987
+ this.preExecute('formField.edit', function (context) {
3988
+ const {
3989
+ formField,
3990
+ properties
3991
+ } = context;
3992
+ const {
3993
+ path,
3994
+ type
3995
+ } = formField;
3996
+ const {
3997
+ config
3998
+ } = formFields.get(type);
3999
+ if (config.pathed && 'path' in properties) {
4000
+ modeling.unclaimPath(formField, path);
4001
+ modeling.claimPath(formField, properties.path);
4002
+ }
4003
+ }, true);
4004
+ }
4005
+ }
4006
+ PathBehavior.$inject = ['eventBus', 'modeling', 'formFields'];
3960
4007
 
3961
4008
  class ValidateBehavior extends CommandInterceptor {
3962
4009
  constructor(eventBus) {
@@ -3965,7 +4012,6 @@ class ValidateBehavior extends CommandInterceptor {
3965
4012
  /**
3966
4013
  * Remove custom validation if <validationType> is about to be added.
3967
4014
  */
3968
- // @ts-ignore-next-line
3969
4015
  this.preExecute('formField.edit', function (context) {
3970
4016
  const {
3971
4017
  properties
@@ -3988,9 +4034,10 @@ class ValidateBehavior extends CommandInterceptor {
3988
4034
  ValidateBehavior.$inject = ['eventBus'];
3989
4035
 
3990
4036
  var behaviorModule = {
3991
- __init__: ['idBehavior', 'keyBehavior', 'validateBehavior'],
4037
+ __init__: ['idBehavior', 'keyBehavior', 'pathBehavior', 'validateBehavior'],
3992
4038
  idBehavior: ['type', IdBehavior],
3993
4039
  keyBehavior: ['type', KeyBehavior],
4040
+ pathBehavior: ['type', PathBehavior],
3994
4041
  validateBehavior: ['type', ValidateBehavior]
3995
4042
  };
3996
4043
 
@@ -4661,6 +4708,33 @@ DeleteIcon.defaultProps = {
4661
4708
  width: "16",
4662
4709
  height: "16"
4663
4710
  };
4711
+ var DragIcon = function DragIcon(props) {
4712
+ return jsxRuntime.jsxs("svg", {
4713
+ ...props,
4714
+ children: [jsxRuntime.jsx("path", {
4715
+ fill: "#fff",
4716
+ style: {
4717
+ mixBlendMode: "multiply"
4718
+ },
4719
+ d: "M0 0h16v16H0z"
4720
+ }), jsxRuntime.jsx("path", {
4721
+ fill: "#fff",
4722
+ style: {
4723
+ mixBlendMode: "multiply"
4724
+ },
4725
+ d: "M0 0h16v16H0z"
4726
+ }), jsxRuntime.jsx("path", {
4727
+ d: "M7 3H5v2h2V3zm4 0H9v2h2V3zM7 7H5v2h2V7zm4 0H9v2h2V7zm-4 4H5v2h2v-2zm4 0H9v2h2v-2z",
4728
+ fill: "#161616"
4729
+ })]
4730
+ });
4731
+ };
4732
+ DragIcon.defaultProps = {
4733
+ width: "16",
4734
+ height: "16",
4735
+ fill: "none",
4736
+ xmlns: "http://www.w3.org/2000/svg"
4737
+ };
4664
4738
  var ExternalLinkIcon = function ExternalLinkIcon(props) {
4665
4739
  return jsxRuntime.jsx("svg", {
4666
4740
  ...props,
@@ -4668,7 +4742,7 @@ var ExternalLinkIcon = function ExternalLinkIcon(props) {
4668
4742
  fillRule: "evenodd",
4669
4743
  clipRule: "evenodd",
4670
4744
  d: "M12.637 12.637v-4.72h1.362v4.721c0 .36-.137.676-.411.95-.275.275-.591.412-.95.412H3.362c-.38 0-.703-.132-.967-.396A1.315 1.315 0 0 1 2 12.638V3.362c0-.38.132-.703.396-.967S2.982 2 3.363 2h4.553v1.363H3.363v9.274h9.274ZM14 2H9.28l-.001 1.362h2.408L5.065 9.984l.95.95 6.622-6.622v2.409H14V2Z",
4671
- fill: "#818798"
4745
+ fill: "currentcolor"
4672
4746
  })
4673
4747
  });
4674
4748
  };
@@ -4811,7 +4885,7 @@ function TooltipWrapper(props) {
4811
4885
  return jsxRuntime.jsx(Tooltip, {
4812
4886
  ...props,
4813
4887
  value: value,
4814
- forId: prefixId$8(forId)
4888
+ forId: prefixId$9(forId)
4815
4889
  });
4816
4890
  }
4817
4891
  function Tooltip(props) {
@@ -4929,7 +5003,7 @@ function getTooltipPosition(refElement) {
4929
5003
  function isHovered(element) {
4930
5004
  return element.matches(':hover');
4931
5005
  }
4932
- function prefixId$8(id) {
5006
+ function prefixId$9(id) {
4933
5007
  return `bio-properties-panel-${id}`;
4934
5008
  }
4935
5009
 
@@ -5302,638 +5376,739 @@ function Placeholder(props) {
5302
5376
  })
5303
5377
  });
5304
5378
  }
5305
- const DEFAULT_LAYOUT = {};
5306
- const DEFAULT_DESCRIPTION = {};
5307
- const DEFAULT_TOOLTIP = {};
5379
+ function Description$1(props) {
5380
+ const {
5381
+ element,
5382
+ forId,
5383
+ value
5384
+ } = props;
5385
+ const contextDescription = useDescriptionContext(forId, element);
5386
+ const description = value || contextDescription;
5387
+ if (description) {
5388
+ return jsxRuntime.jsx("div", {
5389
+ class: "bio-properties-panel-description",
5390
+ children: description
5391
+ });
5392
+ }
5393
+ }
5394
+ const noop$6 = () => {};
5308
5395
 
5309
5396
  /**
5310
- * @typedef { {
5311
- * component: import('preact').Component,
5312
- * id: String,
5313
- * isEdited?: Function
5314
- * } } EntryDefinition
5315
- *
5316
- * @typedef { {
5317
- * autoFocusEntry: String,
5318
- * autoOpen?: Boolean,
5319
- * entries: Array<EntryDefinition>,
5320
- * id: String,
5321
- * label: String,
5322
- * remove: (event: MouseEvent) => void
5323
- * } } ListItemDefinition
5324
- *
5325
- * @typedef { {
5326
- * add: (event: MouseEvent) => void,
5327
- * component: import('preact').Component,
5328
- * element: Object,
5329
- * id: String,
5330
- * items: Array<ListItemDefinition>,
5331
- * label: String,
5332
- * shouldSort?: Boolean,
5333
- * shouldOpen?: Boolean
5334
- * } } ListGroupDefinition
5335
- *
5336
- * @typedef { {
5337
- * component?: import('preact').Component,
5338
- * entries: Array<EntryDefinition>,
5339
- * id: String,
5340
- * label: String,
5341
- * shouldOpen?: Boolean
5342
- * } } GroupDefinition
5343
- *
5344
- * @typedef { {
5345
- * [id: String]: GetDescriptionFunction
5346
- * } } DescriptionConfig
5347
- *
5348
- * @typedef { {
5349
- * [id: String]: GetTooltipFunction
5350
- * } } TooltipConfig
5351
- *
5352
- * @callback { {
5353
- * @param {string} id
5354
- * @param {Object} element
5355
- * @returns {string}
5356
- * } } GetDescriptionFunction
5357
- *
5358
- * @callback { {
5359
- * @param {string} id
5360
- * @param {Object} element
5361
- * @returns {string}
5362
- * } } GetTooltipFunction
5363
- *
5364
- * @typedef { {
5365
- * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
5366
- * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
5367
- * } } PlaceholderProvider
5368
- *
5369
- */
5370
-
5371
- /**
5372
- * A basic properties panel component. Describes *how* content will be rendered, accepts
5373
- * data from implementor to describe *what* will be rendered.
5374
- *
5375
- * @param {Object} props
5376
- * @param {Object|Array} props.element
5377
- * @param {import('./components/Header').HeaderProvider} props.headerProvider
5378
- * @param {PlaceholderProvider} [props.placeholderProvider]
5379
- * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
5380
- * @param {Object} [props.layoutConfig]
5381
- * @param {Function} [props.layoutChanged]
5382
- * @param {DescriptionConfig} [props.descriptionConfig]
5383
- * @param {Function} [props.descriptionLoaded]
5384
- * @param {TooltipConfig} [props.tooltipConfig]
5385
- * @param {Function} [props.tooltipLoaded]
5386
- * @param {Object} [props.eventBus]
5397
+ * Buffer `.focus()` calls while the editor is not initialized.
5398
+ * Set Focus inside when the editor is ready.
5387
5399
  */
5388
- function PropertiesPanel(props) {
5400
+ const useBufferedFocus$1 = function (editor, ref) {
5401
+ const [buffer, setBuffer] = hooks.useState(undefined);
5402
+ ref.current = hooks.useMemo(() => ({
5403
+ focus: offset => {
5404
+ if (editor) {
5405
+ editor.focus(offset);
5406
+ } else {
5407
+ if (typeof offset === 'undefined') {
5408
+ offset = Infinity;
5409
+ }
5410
+ setBuffer(offset);
5411
+ }
5412
+ }
5413
+ }), [editor]);
5414
+ hooks.useEffect(() => {
5415
+ if (typeof buffer !== 'undefined' && editor) {
5416
+ editor.focus(buffer);
5417
+ setBuffer(false);
5418
+ }
5419
+ }, [editor, buffer]);
5420
+ };
5421
+ const CodeEditor$1 = React.forwardRef((props, ref) => {
5389
5422
  const {
5390
- element,
5391
- headerProvider,
5392
- placeholderProvider,
5393
- groups,
5394
- layoutConfig,
5395
- layoutChanged,
5396
- descriptionConfig,
5397
- descriptionLoaded,
5398
- tooltipConfig,
5399
- tooltipLoaded,
5400
- eventBus
5423
+ onInput,
5424
+ disabled,
5425
+ tooltipContainer,
5426
+ enableGutters,
5427
+ value,
5428
+ onLint = noop$6,
5429
+ onPopupOpen = noop$6,
5430
+ popupOpen,
5431
+ contentAttributes = {},
5432
+ hostLanguage = null,
5433
+ singleLine = false
5401
5434
  } = props;
5402
-
5403
- // set-up layout context
5404
- const [layout, setLayout] = hooks.useState(createLayout(layoutConfig));
5405
-
5406
- // react to external changes in the layout config
5407
- useUpdateLayoutEffect(() => {
5408
- const newLayout = createLayout(layoutConfig);
5409
- setLayout(newLayout);
5410
- }, [layoutConfig]);
5435
+ const inputRef = hooks.useRef();
5436
+ const [editor, setEditor] = hooks.useState();
5437
+ const [localValue, setLocalValue] = hooks.useState(value || '');
5438
+ useBufferedFocus$1(editor, ref);
5439
+ const handleInput = useStaticCallback(newValue => {
5440
+ onInput(newValue);
5441
+ setLocalValue(newValue);
5442
+ });
5411
5443
  hooks.useEffect(() => {
5412
- if (typeof layoutChanged === 'function') {
5413
- layoutChanged(layout);
5444
+ let editor;
5445
+ editor = new feelers.FeelersEditor({
5446
+ container: inputRef.current,
5447
+ onChange: handleInput,
5448
+ value: localValue,
5449
+ onLint,
5450
+ contentAttributes,
5451
+ tooltipContainer,
5452
+ enableGutters,
5453
+ hostLanguage,
5454
+ singleLine
5455
+ });
5456
+ setEditor(editor);
5457
+ return () => {
5458
+ onLint([]);
5459
+ inputRef.current.innerHTML = '';
5460
+ setEditor(null);
5461
+ };
5462
+ }, []);
5463
+ hooks.useEffect(() => {
5464
+ if (!editor) {
5465
+ return;
5414
5466
  }
5415
- }, [layout, layoutChanged]);
5416
- const getLayoutForKey = (key, defaultValue) => {
5417
- return minDash.get(layout, key, defaultValue);
5418
- };
5419
- const setLayoutForKey = (key, config) => {
5420
- const newLayout = minDash.assign({}, layout);
5421
- minDash.set(newLayout, key, config);
5422
- setLayout(newLayout);
5423
- };
5424
- const layoutContext = {
5425
- layout,
5426
- setLayout,
5427
- getLayoutForKey,
5428
- setLayoutForKey
5467
+ if (value === localValue) {
5468
+ return;
5469
+ }
5470
+ editor.setValue(value);
5471
+ setLocalValue(value);
5472
+ }, [value]);
5473
+ const handleClick = () => {
5474
+ ref.current.focus();
5429
5475
  };
5476
+ return jsxRuntime.jsxs("div", {
5477
+ class: classnames('bio-properties-panel-feelers-editor-container', popupOpen ? 'popupOpen' : null),
5478
+ children: [jsxRuntime.jsx("div", {
5479
+ class: "bio-properties-panel-feelers-editor__open-popup-placeholder",
5480
+ children: "Opened in editor"
5481
+ }), jsxRuntime.jsx("div", {
5482
+ name: props.name,
5483
+ class: classnames('bio-properties-panel-feelers-editor bio-properties-panel-input', localValue ? 'edited' : null, disabled ? 'disabled' : null),
5484
+ ref: inputRef,
5485
+ onClick: handleClick
5486
+ }), jsxRuntime.jsx("button", {
5487
+ title: "Open pop-up editor",
5488
+ class: "bio-properties-panel-open-feel-popup",
5489
+ onClick: () => onPopupOpen('feelers'),
5490
+ children: jsxRuntime.jsx(ExternalLinkIcon, {})
5491
+ })]
5492
+ });
5493
+ });
5494
+ const noop$5 = () => {};
5430
5495
 
5431
- // set-up description context
5432
- const description = hooks.useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
5496
+ /**
5497
+ * Buffer `.focus()` calls while the editor is not initialized.
5498
+ * Set Focus inside when the editor is ready.
5499
+ */
5500
+ const useBufferedFocus = function (editor, ref) {
5501
+ const [buffer, setBuffer] = hooks.useState(undefined);
5502
+ ref.current = hooks.useMemo(() => ({
5503
+ focus: offset => {
5504
+ if (editor) {
5505
+ editor.focus(offset);
5506
+ } else {
5507
+ if (typeof offset === 'undefined') {
5508
+ offset = Infinity;
5509
+ }
5510
+ setBuffer(offset);
5511
+ }
5512
+ }
5513
+ }), [editor]);
5433
5514
  hooks.useEffect(() => {
5434
- if (typeof descriptionLoaded === 'function') {
5435
- descriptionLoaded(description);
5515
+ if (typeof buffer !== 'undefined' && editor) {
5516
+ editor.focus(buffer);
5517
+ setBuffer(false);
5436
5518
  }
5437
- }, [description, descriptionLoaded]);
5438
- const getDescriptionForId = (id, element) => {
5439
- return description[id] && description[id](element);
5440
- };
5441
- const descriptionContext = {
5442
- description,
5443
- getDescriptionForId
5444
- };
5519
+ }, [editor, buffer]);
5520
+ };
5521
+ const CodeEditor = React.forwardRef((props, ref) => {
5522
+ const {
5523
+ enableGutters,
5524
+ value,
5525
+ onInput,
5526
+ onFeelToggle = noop$5,
5527
+ onLint = noop$5,
5528
+ onPopupOpen = noop$5,
5529
+ popupOpen,
5530
+ disabled,
5531
+ tooltipContainer,
5532
+ variables
5533
+ } = props;
5534
+ const inputRef = hooks.useRef();
5535
+ const [editor, setEditor] = hooks.useState();
5536
+ const [localValue, setLocalValue] = hooks.useState(value || '');
5537
+ useBufferedFocus(editor, ref);
5538
+ const handleInput = useStaticCallback(newValue => {
5539
+ onInput(newValue);
5540
+ setLocalValue(newValue);
5541
+ });
5542
+ hooks.useEffect(() => {
5543
+ let editor;
5445
5544
 
5446
- // set-up tooltip context
5447
- const tooltip = hooks.useMemo(() => createTooltipContext(tooltipConfig), [tooltipConfig]);
5545
+ /* Trigger FEEL toggle when
5546
+ *
5547
+ * - `backspace` is pressed
5548
+ * - AND the cursor is at the beginning of the input
5549
+ */
5550
+ const onKeyDown = e => {
5551
+ if (e.key !== 'Backspace' || !editor) {
5552
+ return;
5553
+ }
5554
+ const selection = editor.getSelection();
5555
+ const range = selection.ranges[selection.mainIndex];
5556
+ if (range.from === 0 && range.to === 0) {
5557
+ onFeelToggle();
5558
+ }
5559
+ };
5560
+ editor = new FeelEditor({
5561
+ container: inputRef.current,
5562
+ onChange: handleInput,
5563
+ onKeyDown: onKeyDown,
5564
+ onLint: onLint,
5565
+ tooltipContainer: tooltipContainer,
5566
+ value: localValue,
5567
+ variables: variables,
5568
+ extensions: [...(enableGutters ? [view.lineNumbers()] : [])]
5569
+ });
5570
+ setEditor(editor);
5571
+ return () => {
5572
+ onLint([]);
5573
+ inputRef.current.innerHTML = '';
5574
+ setEditor(null);
5575
+ };
5576
+ }, []);
5448
5577
  hooks.useEffect(() => {
5449
- if (typeof tooltipLoaded === 'function') {
5450
- tooltipLoaded(tooltip);
5578
+ if (!editor) {
5579
+ return;
5451
5580
  }
5452
- }, [tooltip, tooltipLoaded]);
5453
- const getTooltipForId = (id, element) => {
5454
- return tooltip[id] && tooltip[id](element);
5455
- };
5456
- const tooltipContext = {
5457
- tooltip,
5458
- getTooltipForId
5459
- };
5460
- const [errors, setErrors] = hooks.useState({});
5461
- const onSetErrors = ({
5462
- errors
5463
- }) => setErrors(errors);
5464
- useEvent('propertiesPanel.setErrors', onSetErrors, eventBus);
5465
- const errorsContext = {
5466
- errors
5467
- };
5468
- const eventContext = {
5469
- eventBus
5470
- };
5471
- const propertiesPanelContext = {
5472
- element
5581
+ if (value === localValue) {
5582
+ return;
5583
+ }
5584
+ editor.setValue(value);
5585
+ setLocalValue(value);
5586
+ }, [value]);
5587
+ hooks.useEffect(() => {
5588
+ if (!editor) {
5589
+ return;
5590
+ }
5591
+ editor.setVariables(variables);
5592
+ }, [variables]);
5593
+ const handleClick = () => {
5594
+ ref.current.focus();
5473
5595
  };
5474
-
5475
- // empty state
5476
- if (placeholderProvider && !element) {
5477
- return jsxRuntime.jsx(Placeholder, {
5478
- ...placeholderProvider.getEmpty()
5479
- });
5480
- }
5481
-
5482
- // multiple state
5483
- if (placeholderProvider && minDash.isArray(element)) {
5484
- return jsxRuntime.jsx(Placeholder, {
5485
- ...placeholderProvider.getMultiple()
5486
- });
5596
+ return jsxRuntime.jsxs("div", {
5597
+ class: classnames('bio-properties-panel-feel-editor-container', disabled ? 'disabled' : null, popupOpen ? 'popupOpen' : null),
5598
+ children: [jsxRuntime.jsx("div", {
5599
+ class: "bio-properties-panel-feel-editor__open-popup-placeholder",
5600
+ children: "Opened in editor"
5601
+ }), jsxRuntime.jsx("div", {
5602
+ name: props.name,
5603
+ class: classnames('bio-properties-panel-input', localValue ? 'edited' : null),
5604
+ ref: inputRef,
5605
+ onClick: handleClick
5606
+ }), jsxRuntime.jsx("button", {
5607
+ title: "Open pop-up editor",
5608
+ class: "bio-properties-panel-open-feel-popup",
5609
+ onClick: () => onPopupOpen(),
5610
+ children: jsxRuntime.jsx(ExternalLinkIcon, {})
5611
+ })]
5612
+ });
5613
+ });
5614
+ function FeelIndicator(props) {
5615
+ const {
5616
+ active
5617
+ } = props;
5618
+ if (!active) {
5619
+ return null;
5487
5620
  }
5488
- return jsxRuntime.jsx(LayoutContext.Provider, {
5489
- value: propertiesPanelContext,
5490
- children: jsxRuntime.jsx(ErrorsContext.Provider, {
5491
- value: errorsContext,
5492
- children: jsxRuntime.jsx(DescriptionContext.Provider, {
5493
- value: descriptionContext,
5494
- children: jsxRuntime.jsx(TooltipContext.Provider, {
5495
- value: tooltipContext,
5496
- children: jsxRuntime.jsx(LayoutContext.Provider, {
5497
- value: layoutContext,
5498
- children: jsxRuntime.jsx(EventContext.Provider, {
5499
- value: eventContext,
5500
- children: jsxRuntime.jsxs("div", {
5501
- class: "bio-properties-panel",
5502
- children: [jsxRuntime.jsx(Header, {
5503
- element: element,
5504
- headerProvider: headerProvider
5505
- }), jsxRuntime.jsx("div", {
5506
- class: "bio-properties-panel-scroll-container",
5507
- children: groups.map(group => {
5508
- const {
5509
- component: Component = Group,
5510
- id
5511
- } = group;
5512
- return preact.createElement(Component, {
5513
- ...group,
5514
- key: id,
5515
- element: element
5516
- });
5517
- })
5518
- })]
5519
- })
5520
- })
5521
- })
5522
- })
5523
- })
5524
- })
5621
+ return jsxRuntime.jsx("span", {
5622
+ class: "bio-properties-panel-feel-indicator",
5623
+ children: "="
5525
5624
  });
5526
5625
  }
5626
+ const noop$4 = () => {};
5527
5627
 
5528
- // helpers //////////////////
5628
+ /**
5629
+ * @param {Object} props
5630
+ * @param {Object} props.label
5631
+ * @param {String} props.feel
5632
+ */
5633
+ function FeelIcon(props) {
5634
+ const {
5635
+ feel = false,
5636
+ active,
5637
+ disabled = false,
5638
+ onClick = noop$4
5639
+ } = props;
5640
+ const feelRequiredLabel = 'FEEL expression is mandatory';
5641
+ const feelOptionalLabel = `Click to ${active ? 'remove' : 'set a'} dynamic value with FEEL expression`;
5642
+ const handleClick = e => {
5643
+ onClick(e);
5529
5644
 
5530
- function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
5531
- return {
5532
- ...defaults,
5533
- ...overrides
5645
+ // when pointer event was created from keyboard, keep focus on button
5646
+ if (!e.pointerType) {
5647
+ e.stopPropagation();
5648
+ }
5534
5649
  };
5650
+ return jsxRuntime.jsx("button", {
5651
+ class: classnames('bio-properties-panel-feel-icon', active ? 'active' : null, feel === 'required' ? 'required' : 'optional'),
5652
+ onClick: handleClick,
5653
+ disabled: feel === 'required' || disabled,
5654
+ title: feel === 'required' ? feelRequiredLabel : feelOptionalLabel,
5655
+ children: jsxRuntime.jsx(FeelIcon$1, {})
5656
+ });
5535
5657
  }
5536
- function createDescriptionContext(overrides = {}) {
5537
- return {
5538
- ...DEFAULT_DESCRIPTION,
5539
- ...overrides
5540
- };
5658
+ const FeelPopupContext = preact.createContext({
5659
+ open: () => {},
5660
+ close: () => {},
5661
+ source: null
5662
+ });
5663
+
5664
+ /**
5665
+ * Add a dragger that calls back the passed function with
5666
+ * { event, delta } on drag.
5667
+ *
5668
+ * @example
5669
+ *
5670
+ * function dragMove(event, delta) {
5671
+ * // we are dragging (!!)
5672
+ * }
5673
+ *
5674
+ * domElement.addEventListener('dragstart', dragger(dragMove));
5675
+ *
5676
+ * @param {Function} fn
5677
+ * @param {Element} [dragPreview]
5678
+ *
5679
+ * @return {Function} drag start callback function
5680
+ */
5681
+ function createDragger(fn, dragPreview) {
5682
+ let self;
5683
+ let startX, startY;
5684
+
5685
+ /** drag start */
5686
+ function onDragStart(event) {
5687
+ self = this;
5688
+ startX = event.clientX;
5689
+ startY = event.clientY;
5690
+
5691
+ // (1) prevent preview image
5692
+ if (event.dataTransfer) {
5693
+ event.dataTransfer.setDragImage(dragPreview || emptyCanvas(), 0, 0);
5694
+ }
5695
+
5696
+ // (2) setup drag listeners
5697
+
5698
+ // attach drag + cleanup event
5699
+ document.addEventListener('dragover', onDrag);
5700
+ document.addEventListener('dragend', onEnd);
5701
+ document.addEventListener('drop', preventDefault);
5702
+ }
5703
+ function onDrag(event) {
5704
+ const delta = {
5705
+ x: event.clientX - startX,
5706
+ y: event.clientY - startY
5707
+ };
5708
+
5709
+ // call provided fn with event, delta
5710
+ return fn.call(self, event, delta);
5711
+ }
5712
+ function onEnd() {
5713
+ document.removeEventListener('dragover', onDrag);
5714
+ document.removeEventListener('dragend', onEnd);
5715
+ document.removeEventListener('drop', preventDefault);
5716
+ }
5717
+ return onDragStart;
5541
5718
  }
5542
- function createTooltipContext(overrides = {}) {
5543
- return {
5544
- ...DEFAULT_TOOLTIP,
5545
- ...overrides
5546
- };
5719
+ function preventDefault(event) {
5720
+ event.preventDefault();
5721
+ event.stopPropagation();
5547
5722
  }
5548
-
5549
- // hooks //////////////////
5723
+ function emptyCanvas() {
5724
+ return minDom.domify('<canvas width="0" height="0" />');
5725
+ }
5726
+ const noop$3 = () => {};
5550
5727
 
5551
5728
  /**
5552
- * This hook behaves like useLayoutEffect, but does not trigger on the first render.
5729
+ * A generic popup component.
5553
5730
  *
5554
- * @param {Function} effect
5555
- * @param {Array} deps
5731
+ * @param {Object} props
5732
+ * @param {HTMLElement} [props.container]
5733
+ * @param {string} [props.className]
5734
+ * @param {{x: number, y: number}} [props.position]
5735
+ * @param {number} [props.width]
5736
+ * @param {number} [props.height]
5737
+ * @param {Function} props.onClose
5738
+ * @param {Function} [props.onPostActivate]
5739
+ * @param {Function} [props.onPostDeactivate]
5740
+ * @param {boolean} [props.returnFocus]
5741
+ * @param {string} props.title
5556
5742
  */
5557
- function useUpdateLayoutEffect(effect, deps) {
5558
- const isMounted = hooks.useRef(false);
5559
- hooks.useLayoutEffect(() => {
5560
- if (isMounted.current) {
5561
- return effect();
5562
- } else {
5563
- isMounted.current = true;
5743
+ function Popup(props) {
5744
+ const {
5745
+ container,
5746
+ className,
5747
+ position,
5748
+ width,
5749
+ height,
5750
+ onClose,
5751
+ onPostActivate = noop$3,
5752
+ onPostDeactivate = noop$3,
5753
+ returnFocus = true,
5754
+ title
5755
+ } = props;
5756
+ const focusTrapRef = hooks.useRef(null);
5757
+ const popupRef = hooks.useRef(null);
5758
+ const handleKeyPress = event => {
5759
+ if (event.key === 'Escape') {
5760
+ onClose();
5564
5761
  }
5565
- }, deps);
5762
+ };
5763
+
5764
+ // re-activate focus trap on focus
5765
+ const handleFocus = () => {
5766
+ if (focusTrapRef.current) {
5767
+ focusTrapRef.current.activate();
5768
+ }
5769
+ };
5770
+ let style = {};
5771
+ if (position) {
5772
+ style = {
5773
+ ...style,
5774
+ top: position.top + 'px',
5775
+ left: position.left + 'px'
5776
+ };
5777
+ }
5778
+ if (width) {
5779
+ style.width = width + 'px';
5780
+ }
5781
+ if (height) {
5782
+ style.height = height + 'px';
5783
+ }
5784
+ hooks.useEffect(() => {
5785
+ if (popupRef.current) {
5786
+ popupRef.current.addEventListener('keydown', handleKeyPress);
5787
+ }
5788
+ return () => {
5789
+ popupRef.current.removeEventListener('keydown', handleKeyPress);
5790
+ };
5791
+ }, [popupRef]);
5792
+ hooks.useEffect(() => {
5793
+ if (popupRef.current) {
5794
+ popupRef.current.addEventListener('focusin', handleFocus);
5795
+ }
5796
+ return () => {
5797
+ popupRef.current.removeEventListener('focusin', handleFocus);
5798
+ };
5799
+ }, [popupRef]);
5800
+ hooks.useEffect(() => {
5801
+ if (popupRef.current) {
5802
+ focusTrapRef.current = focusTrap__namespace.createFocusTrap(popupRef.current, {
5803
+ clickOutsideDeactivates: true,
5804
+ fallbackFocus: popupRef.current,
5805
+ onPostActivate,
5806
+ onPostDeactivate,
5807
+ returnFocusOnDeactivate: returnFocus
5808
+ });
5809
+ focusTrapRef.current.activate();
5810
+ }
5811
+ return () => focusTrapRef.current && focusTrapRef.current.deactivate();
5812
+ }, [popupRef]);
5813
+ return React.createPortal(jsxRuntime.jsx("div", {
5814
+ "aria-label": title,
5815
+ tabIndex: -1,
5816
+ ref: popupRef,
5817
+ role: "dialog",
5818
+ class: classnames('bio-properties-panel-popup', className),
5819
+ style: style,
5820
+ children: props.children
5821
+ }), container || document.body);
5566
5822
  }
5567
- function CollapsibleEntry(props) {
5823
+ Popup.Title = Title;
5824
+ Popup.Body = Body;
5825
+ Popup.Footer = Footer;
5826
+ function Title(props) {
5568
5827
  const {
5569
- element,
5570
- entries = [],
5571
- id,
5572
- label,
5573
- open: shouldOpen,
5574
- remove
5828
+ children,
5829
+ className,
5830
+ draggable,
5831
+ title,
5832
+ ...rest
5575
5833
  } = props;
5576
- const [open, setOpen] = hooks.useState(shouldOpen);
5577
- const toggleOpen = () => setOpen(!open);
5578
- const {
5579
- onShow
5580
- } = hooks.useContext(LayoutContext);
5581
- const propertiesPanelContext = {
5582
- ...hooks.useContext(LayoutContext),
5583
- onShow: hooks.useCallback(() => {
5584
- setOpen(true);
5585
- if (minDash.isFunction(onShow)) {
5586
- onShow();
5587
- }
5588
- }, [onShow, setOpen])
5589
- };
5590
5834
 
5591
- // todo(pinussilvestrus): translate once we have a translate mechanism for the core
5592
- const placeholderLabel = '<empty>';
5835
+ // we can't use state as we need to
5836
+ // manipulate this inside dragging events
5837
+ const context = hooks.useRef({
5838
+ startPosition: null,
5839
+ newPosition: null
5840
+ });
5841
+ const dragPreviewRef = hooks.useRef();
5842
+ const titleRef = hooks.useRef();
5843
+ const onMove = minDash.throttle((_, delta) => {
5844
+ const {
5845
+ x: dx,
5846
+ y: dy
5847
+ } = delta;
5848
+ const newPosition = {
5849
+ x: context.current.startPosition.x + dx,
5850
+ y: context.current.startPosition.y + dy
5851
+ };
5852
+ const popupParent = getPopupParent(titleRef.current);
5853
+ popupParent.style.top = newPosition.y + 'px';
5854
+ popupParent.style.left = newPosition.x + 'px';
5855
+ });
5856
+ const onMoveStart = event => {
5857
+ // initialize drag handler
5858
+ const onDragStart = createDragger(onMove, dragPreviewRef.current);
5859
+ onDragStart(event);
5860
+ const popupParent = getPopupParent(titleRef.current);
5861
+ const bounds = popupParent.getBoundingClientRect();
5862
+ context.current.startPosition = {
5863
+ x: bounds.left,
5864
+ y: bounds.top
5865
+ };
5866
+ };
5867
+ const onMoveEnd = () => {
5868
+ context.current.newPosition = null;
5869
+ };
5593
5870
  return jsxRuntime.jsxs("div", {
5594
- "data-entry-id": id,
5595
- class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
5596
- children: [jsxRuntime.jsxs("div", {
5597
- class: "bio-properties-panel-collapsible-entry-header",
5598
- onClick: toggleOpen,
5871
+ class: classnames('bio-properties-panel-popup__header', draggable && 'draggable', className),
5872
+ ref: titleRef,
5873
+ draggable: draggable,
5874
+ onDragStart: onMoveStart,
5875
+ onDragEnd: onMoveEnd,
5876
+ ...rest,
5877
+ children: [draggable && jsxRuntime.jsxs(jsxRuntime.Fragment, {
5599
5878
  children: [jsxRuntime.jsx("div", {
5600
- title: label || placeholderLabel,
5601
- class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
5602
- children: label || placeholderLabel
5603
- }), jsxRuntime.jsx("button", {
5604
- title: "Toggle list item",
5605
- class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
5606
- children: jsxRuntime.jsx(ArrowIcon, {
5607
- class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
5608
- })
5609
- }), remove ? jsxRuntime.jsx("button", {
5610
- title: "Delete item",
5611
- class: "bio-properties-panel-remove-entry",
5612
- onClick: remove,
5613
- children: jsxRuntime.jsx(DeleteIcon, {})
5614
- }) : null]
5879
+ ref: dragPreviewRef,
5880
+ class: "bio-properties-panel-popup__drag-preview"
5881
+ }), jsxRuntime.jsx("div", {
5882
+ class: "bio-properties-panel-popup__drag-handle",
5883
+ children: jsxRuntime.jsx(DragIcon, {})
5884
+ })]
5615
5885
  }), jsxRuntime.jsx("div", {
5616
- class: classnames('bio-properties-panel-collapsible-entry-entries', open ? 'open' : ''),
5617
- children: jsxRuntime.jsx(LayoutContext.Provider, {
5618
- value: propertiesPanelContext,
5619
- children: entries.map(entry => {
5620
- const {
5621
- component: Component,
5622
- id
5623
- } = entry;
5624
- return preact.createElement(Component, {
5625
- ...entry,
5626
- element: element,
5627
- key: id
5628
- });
5629
- })
5630
- })
5631
- })]
5886
+ class: "bio-properties-panel-popup__title",
5887
+ children: title
5888
+ }), children]
5632
5889
  });
5633
5890
  }
5634
- function ListItem(props) {
5891
+ function Body(props) {
5635
5892
  const {
5636
- autoFocusEntry,
5637
- autoOpen
5893
+ children,
5894
+ className,
5895
+ ...rest
5638
5896
  } = props;
5639
-
5640
- // focus specified entry on auto open
5641
- hooks.useEffect(() => {
5642
- if (autoOpen && autoFocusEntry) {
5643
- const entry = minDom.query(`[data-entry-id="${autoFocusEntry}"]`);
5644
- const focusableInput = minDom.query('.bio-properties-panel-input', entry);
5645
- if (focusableInput) {
5646
- if (minDash.isFunction(focusableInput.select)) {
5647
- focusableInput.select();
5648
- } else if (minDash.isFunction(focusableInput.focus)) {
5649
- focusableInput.focus();
5650
- }
5651
- }
5652
- }
5653
- }, [autoOpen, autoFocusEntry]);
5654
5897
  return jsxRuntime.jsx("div", {
5655
- class: "bio-properties-panel-list-item",
5656
- children: jsxRuntime.jsx(CollapsibleEntry, {
5657
- ...props,
5658
- open: autoOpen
5659
- })
5898
+ class: classnames('bio-properties-panel-popup__body', className),
5899
+ ...rest,
5900
+ children: children
5660
5901
  });
5661
5902
  }
5662
- const noop$3 = () => {};
5903
+ function Footer(props) {
5904
+ const {
5905
+ children,
5906
+ className,
5907
+ ...rest
5908
+ } = props;
5909
+ return jsxRuntime.jsx("div", {
5910
+ class: classnames('bio-properties-panel-popup__footer', className),
5911
+ ...rest,
5912
+ children: props.children
5913
+ });
5914
+ }
5915
+
5916
+ // helpers //////////////////////
5917
+
5918
+ function getPopupParent(node) {
5919
+ return node.closest('.bio-properties-panel-popup');
5920
+ }
5921
+ const FEEL_POPUP_WIDTH = 700;
5922
+ const FEEL_POPUP_HEIGHT = 250;
5663
5923
 
5664
5924
  /**
5665
- * @param {import('../PropertiesPanel').ListGroupDefinition} props
5925
+ * FEEL popup component, built as a singleton.
5666
5926
  */
5667
- function ListGroup(props) {
5927
+ function FEELPopupRoot(props) {
5668
5928
  const {
5669
- add,
5670
- element,
5671
- id,
5672
- items,
5673
- label,
5674
- shouldOpen = true,
5675
- shouldSort = true
5929
+ element
5676
5930
  } = props;
5677
- const groupRef = hooks.useRef(null);
5678
- const [open, setOpen] = useLayoutState(['groups', id, 'open'], false);
5679
- const [sticky, setSticky] = hooks.useState(false);
5680
- const onShow = hooks.useCallback(() => setOpen(true), [setOpen]);
5681
- const [ordering, setOrdering] = hooks.useState([]);
5682
- const [newItemAdded, setNewItemAdded] = hooks.useState(false);
5683
-
5684
- // Flag to mark that add button was clicked in the last render cycle
5685
- const [addTriggered, setAddTriggered] = hooks.useState(false);
5686
- const prevItems = usePrevious(items);
5687
5931
  const prevElement = usePrevious(element);
5688
- const elementChanged = element !== prevElement;
5689
- const shouldHandleEffects = !elementChanged && (shouldSort || shouldOpen);
5690
-
5691
- // reset initial ordering when element changes (before first render)
5692
- if (elementChanged) {
5693
- setOrdering(createOrdering(shouldSort ? sortItems(items) : items));
5694
- }
5695
-
5696
- // keep ordering in sync to items - and open changes
5697
-
5698
- // (0) set initial ordering from given items
5699
- hooks.useEffect(() => {
5700
- if (!prevItems || !shouldSort) {
5701
- setOrdering(createOrdering(items));
5702
- }
5703
- }, [items, element]);
5704
-
5705
- // (1) items were added
5932
+ const [popupConfig, setPopupConfig] = hooks.useState({});
5933
+ const [open, setOpen] = hooks.useState(false);
5934
+ const [source, setSource] = hooks.useState(null);
5935
+ const [sourceElement, setSourceElement] = hooks.useState(null);
5936
+ const handleOpen = (key, config, _sourceElement) => {
5937
+ setSource(key);
5938
+ setPopupConfig(config);
5939
+ setOpen(true);
5940
+ setSourceElement(_sourceElement);
5941
+ };
5942
+ const handleClose = () => {
5943
+ setOpen(false);
5944
+ setSource(null);
5945
+ };
5946
+ const feelPopupContext = {
5947
+ open: handleOpen,
5948
+ close: handleClose,
5949
+ source
5950
+ };
5951
+
5952
+ // close popup on element change, cf. https://github.com/bpmn-io/properties-panel/issues/270
5706
5953
  hooks.useEffect(() => {
5707
- // reset addTriggered flag
5708
- setAddTriggered(false);
5709
- if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
5710
- let add = [];
5711
- items.forEach(item => {
5712
- if (!ordering.includes(item.id)) {
5713
- add.push(item.id);
5714
- }
5715
- });
5716
- let newOrdering = ordering;
5717
-
5718
- // open if not open, configured and triggered by add button
5719
- //
5720
- // TODO(marstamm): remove once we refactor layout handling for listGroups.
5721
- // Ideally, opening should be handled as part of the `add` callback and
5722
- // not be a concern for the ListGroup component.
5723
- if (addTriggered && !open && shouldOpen) {
5724
- toggleOpen();
5725
- }
5726
-
5727
- // filter when not open and configured
5728
- if (!open && shouldSort) {
5729
- newOrdering = createOrdering(sortItems(items));
5730
- }
5731
-
5732
- // add new items on top or bottom depending on sorting behavior
5733
- newOrdering = newOrdering.filter(item => !add.includes(item));
5734
- if (shouldSort) {
5735
- newOrdering.unshift(...add);
5736
- } else {
5737
- newOrdering.push(...add);
5738
- }
5739
- setOrdering(newOrdering);
5740
- setNewItemAdded(addTriggered);
5741
- } else {
5742
- setNewItemAdded(false);
5954
+ if (element && element !== prevElement) {
5955
+ handleClose();
5743
5956
  }
5744
- }, [items, open, shouldHandleEffects, addTriggered]);
5745
-
5746
- // (2) sort items on open if shouldSort is set
5957
+ }, [element]);
5958
+ return jsxRuntime.jsxs(FeelPopupContext.Provider, {
5959
+ value: feelPopupContext,
5960
+ children: [open && jsxRuntime.jsx(FeelPopupComponent, {
5961
+ onClose: handleClose,
5962
+ sourceElement: sourceElement,
5963
+ ...popupConfig
5964
+ }), props.children]
5965
+ });
5966
+ }
5967
+ function FeelPopupComponent(props) {
5968
+ const {
5969
+ id,
5970
+ hostLanguage,
5971
+ onInput,
5972
+ onClose,
5973
+ position,
5974
+ singleLine,
5975
+ sourceElement,
5976
+ title,
5977
+ tooltipContainer,
5978
+ type,
5979
+ value,
5980
+ variables
5981
+ } = props;
5982
+ const editorRef = hooks.useRef();
5983
+ const handleSetReturnFocus = () => {
5984
+ sourceElement && sourceElement.focus();
5985
+ };
5747
5986
  hooks.useEffect(() => {
5748
- if (shouldSort && open && !newItemAdded) {
5749
- setOrdering(createOrdering(sortItems(items)));
5750
- }
5751
- }, [open, shouldSort]);
5987
+ const editor = editorRef.current;
5988
+ if (editor) {
5989
+ editor.focus();
5990
+ }
5991
+ }, [editorRef, id]);
5992
+ return jsxRuntime.jsxs(Popup, {
5993
+ className: "bio-properties-panel-feel-popup",
5994
+ position: position,
5995
+ title: title,
5996
+ onClose: onClose
5997
+
5998
+ // handle focus manually on deactivate
5999
+ ,
6000
+
6001
+ returnFocus: false,
6002
+ onPostDeactivate: handleSetReturnFocus,
6003
+ height: FEEL_POPUP_HEIGHT,
6004
+ width: FEEL_POPUP_WIDTH,
6005
+ children: [jsxRuntime.jsx(Popup.Title, {
6006
+ title: title,
6007
+ draggable: true
6008
+ }), jsxRuntime.jsx(Popup.Body, {
6009
+ children: jsxRuntime.jsxs("div", {
6010
+ class: "bio-properties-panel-feel-popup__body",
6011
+ children: [type === 'feel' && jsxRuntime.jsx(CodeEditor, {
6012
+ enableGutters: true,
6013
+ id: prefixId$8(id),
6014
+ name: id,
6015
+ onInput: onInput,
6016
+ value: value,
6017
+ variables: variables,
6018
+ ref: editorRef,
6019
+ tooltipContainer: tooltipContainer
6020
+ }), type === 'feelers' && jsxRuntime.jsx(CodeEditor$1, {
6021
+ id: prefixId$8(id),
6022
+ contentAttributes: {
6023
+ 'aria-label': title
6024
+ },
6025
+ enableGutters: true,
6026
+ hostLanguage: hostLanguage,
6027
+ name: id,
6028
+ onInput: onInput,
6029
+ value: value,
6030
+ ref: editorRef,
6031
+ singleLine: singleLine,
6032
+ tooltipContainer: tooltipContainer
6033
+ })]
6034
+ })
6035
+ }), jsxRuntime.jsx(Popup.Footer, {
6036
+ children: jsxRuntime.jsx("button", {
6037
+ onClick: onClose,
6038
+ title: "Close pop-up editor",
6039
+ class: "bio-properties-panel-feel-popup__close-btn",
6040
+ children: "Close"
6041
+ })
6042
+ })]
6043
+ });
6044
+ }
5752
6045
 
5753
- // (3) items were deleted
5754
- hooks.useEffect(() => {
5755
- if (shouldHandleEffects && prevItems && items.length < prevItems.length) {
5756
- let keep = [];
5757
- ordering.forEach(o => {
5758
- if (getItem(items, o)) {
5759
- keep.push(o);
5760
- }
5761
- });
5762
- setOrdering(keep);
5763
- }
5764
- }, [items, shouldHandleEffects]);
6046
+ // helpers /////////////////
5765
6047
 
5766
- // set css class when group is sticky to top
5767
- useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);
5768
- const toggleOpen = () => setOpen(!open);
5769
- const hasItems = !!items.length;
5770
- const propertiesPanelContext = {
5771
- ...hooks.useContext(LayoutContext),
5772
- onShow
6048
+ function prefixId$8(id) {
6049
+ return `bio-properties-panel-${id}`;
6050
+ }
6051
+ function ToggleSwitch(props) {
6052
+ const {
6053
+ id,
6054
+ label,
6055
+ onInput,
6056
+ value,
6057
+ switcherLabel,
6058
+ inline,
6059
+ onFocus,
6060
+ onBlur,
6061
+ inputRef,
6062
+ tooltip
6063
+ } = props;
6064
+ const [localValue, setLocalValue] = hooks.useState(value);
6065
+ const handleInputCallback = async () => {
6066
+ onInput(!value);
5773
6067
  };
5774
- const handleAddClick = e => {
5775
- setAddTriggered(true);
5776
- add(e);
6068
+ const handleInput = e => {
6069
+ handleInputCallback();
6070
+ setLocalValue(e.target.value);
5777
6071
  };
5778
- const allErrors = useErrors();
5779
- const hasError = items.some(item => {
5780
- if (allErrors[item.id]) {
5781
- return true;
5782
- }
5783
- if (!item.entries) {
6072
+ hooks.useEffect(() => {
6073
+ if (value === localValue) {
5784
6074
  return;
5785
6075
  }
5786
-
5787
- // also check if the error is nested, e.g. for name-value entries
5788
- return item.entries.some(entry => allErrors[entry.id]);
5789
- });
6076
+ setLocalValue(value);
6077
+ }, [value]);
5790
6078
  return jsxRuntime.jsxs("div", {
5791
- class: "bio-properties-panel-group",
5792
- "data-group-id": 'group-' + id,
5793
- ref: groupRef,
5794
- children: [jsxRuntime.jsxs("div", {
5795
- class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
5796
- onClick: hasItems ? toggleOpen : noop$3,
5797
- children: [jsxRuntime.jsx("div", {
5798
- title: label,
5799
- class: "bio-properties-panel-group-header-title",
5800
- children: jsxRuntime.jsx(TooltipWrapper, {
5801
- value: props.tooltip,
5802
- forId: 'group-' + id,
5803
- element: element,
5804
- parent: groupRef,
5805
- children: label
5806
- })
5807
- }), jsxRuntime.jsxs("div", {
5808
- class: "bio-properties-panel-group-header-buttons",
5809
- children: [add ? jsxRuntime.jsxs("button", {
5810
- title: "Create new list item",
5811
- class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
5812
- onClick: handleAddClick,
5813
- children: [jsxRuntime.jsx(CreateIcon, {}), !hasItems ? jsxRuntime.jsx("span", {
5814
- class: "bio-properties-panel-add-entry-label",
5815
- children: "Create"
5816
- }) : null]
5817
- }) : null, hasItems ? jsxRuntime.jsx("div", {
5818
- title: `List contains ${items.length} item${items.length != 1 ? 's' : ''}`,
5819
- class: classnames('bio-properties-panel-list-badge', hasError ? 'bio-properties-panel-list-badge--error' : ''),
5820
- children: items.length
5821
- }) : null, hasItems ? jsxRuntime.jsx("button", {
5822
- title: "Toggle section",
5823
- class: "bio-properties-panel-group-header-button bio-properties-panel-arrow",
5824
- children: jsxRuntime.jsx(ArrowIcon, {
5825
- class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
5826
- })
5827
- }) : null]
5828
- })]
5829
- }), jsxRuntime.jsx("div", {
5830
- class: classnames('bio-properties-panel-list', open && hasItems ? 'open' : ''),
5831
- children: jsxRuntime.jsx(LayoutContext.Provider, {
5832
- value: propertiesPanelContext,
5833
- children: ordering.map((o, index) => {
5834
- const item = getItem(items, o);
5835
- if (!item) {
5836
- return;
5837
- }
5838
- const {
5839
- id
5840
- } = item;
5841
-
5842
- // if item was added, open it
5843
- // Existing items will not be affected as autoOpen is only applied on first render
5844
- const autoOpen = newItemAdded;
5845
- return preact.createElement(ListItem, {
5846
- ...item,
5847
- autoOpen: autoOpen,
5848
- element: element,
5849
- index: index,
5850
- key: id
5851
- });
5852
- })
5853
- })
5854
- })]
5855
- });
5856
- }
5857
-
5858
- // helpers ////////////////////
5859
-
5860
- /**
5861
- * Sorts given items alphanumeric by label
5862
- */
5863
- function sortItems(items) {
5864
- return minDash.sortBy(items, i => i.label.toLowerCase());
5865
- }
5866
- function getItem(items, id) {
5867
- return minDash.find(items, i => i.id === id);
5868
- }
5869
- function createOrdering(items) {
5870
- return items.map(i => i.id);
5871
- }
5872
- function Description$1(props) {
5873
- const {
5874
- element,
5875
- forId,
5876
- value
5877
- } = props;
5878
- const contextDescription = useDescriptionContext(forId, element);
5879
- const description = value || contextDescription;
5880
- if (description) {
5881
- return jsxRuntime.jsx("div", {
5882
- class: "bio-properties-panel-description",
5883
- children: description
5884
- });
5885
- }
5886
- }
5887
- function Checkbox(props) {
5888
- const {
5889
- id,
5890
- label,
5891
- onChange,
5892
- disabled,
5893
- value = false,
5894
- onFocus,
5895
- onBlur,
5896
- tooltip
5897
- } = props;
5898
- const [localValue, setLocalValue] = hooks.useState(value);
5899
- const handleChangeCallback = ({
5900
- target
5901
- }) => {
5902
- onChange(target.checked);
5903
- };
5904
- const handleChange = e => {
5905
- handleChangeCallback(e);
5906
- setLocalValue(e.target.value);
5907
- };
5908
- hooks.useEffect(() => {
5909
- if (value === localValue) {
5910
- return;
5911
- }
5912
- setLocalValue(value);
5913
- }, [value]);
5914
- const ref = useShowEntryEvent(id);
5915
- return jsxRuntime.jsxs("div", {
5916
- class: "bio-properties-panel-checkbox",
5917
- children: [jsxRuntime.jsx("input", {
5918
- ref: ref,
5919
- id: prefixId$7(id),
5920
- name: id,
5921
- onFocus: onFocus,
5922
- onBlur: onBlur,
5923
- type: "checkbox",
5924
- class: "bio-properties-panel-input",
5925
- onChange: handleChange,
5926
- checked: localValue,
5927
- disabled: disabled
5928
- }), jsxRuntime.jsx("label", {
5929
- for: prefixId$7(id),
6079
+ class: classnames('bio-properties-panel-toggle-switch', {
6080
+ inline
6081
+ }),
6082
+ children: [jsxRuntime.jsx("label", {
5930
6083
  class: "bio-properties-panel-label",
6084
+ for: prefixId$7(id),
5931
6085
  children: jsxRuntime.jsx(TooltipWrapper, {
5932
6086
  value: tooltip,
5933
6087
  forId: id,
5934
6088
  element: props.element,
5935
6089
  children: label
5936
6090
  })
6091
+ }), jsxRuntime.jsxs("div", {
6092
+ class: "bio-properties-panel-field-wrapper",
6093
+ children: [jsxRuntime.jsxs("label", {
6094
+ class: "bio-properties-panel-toggle-switch__switcher",
6095
+ children: [jsxRuntime.jsx("input", {
6096
+ ref: inputRef,
6097
+ id: prefixId$7(id),
6098
+ class: "bio-properties-panel-input",
6099
+ type: "checkbox",
6100
+ onFocus: onFocus,
6101
+ onBlur: onBlur,
6102
+ name: id,
6103
+ onInput: handleInput,
6104
+ checked: !!localValue
6105
+ }), jsxRuntime.jsx("span", {
6106
+ class: "bio-properties-panel-toggle-switch__slider"
6107
+ })]
6108
+ }), switcherLabel && jsxRuntime.jsx("p", {
6109
+ class: "bio-properties-panel-toggle-switch__label",
6110
+ children: switcherLabel
6111
+ })]
5937
6112
  })]
5938
6113
  });
5939
6114
  }
@@ -5944,44 +6119,43 @@ function Checkbox(props) {
5944
6119
  * @param {String} props.id
5945
6120
  * @param {String} props.description
5946
6121
  * @param {String} props.label
6122
+ * @param {String} props.switcherLabel
6123
+ * @param {Boolean} props.inline
5947
6124
  * @param {Function} props.getValue
5948
6125
  * @param {Function} props.setValue
5949
6126
  * @param {Function} props.onFocus
5950
6127
  * @param {Function} props.onBlur
5951
6128
  * @param {string|import('preact').Component} props.tooltip
5952
- * @param {boolean} [props.disabled]
5953
6129
  */
5954
- function CheckboxEntry(props) {
6130
+ function ToggleSwitchEntry(props) {
5955
6131
  const {
5956
6132
  element,
5957
6133
  id,
5958
6134
  description,
5959
6135
  label,
6136
+ switcherLabel,
6137
+ inline,
5960
6138
  getValue,
5961
6139
  setValue,
5962
- disabled,
5963
6140
  onFocus,
5964
6141
  onBlur,
5965
6142
  tooltip
5966
6143
  } = props;
5967
6144
  const value = getValue(element);
5968
- const error = useError(id);
5969
6145
  return jsxRuntime.jsxs("div", {
5970
- class: "bio-properties-panel-entry bio-properties-panel-checkbox-entry",
6146
+ class: "bio-properties-panel-entry bio-properties-panel-toggle-switch-entry",
5971
6147
  "data-entry-id": id,
5972
- children: [jsxRuntime.jsx(Checkbox, {
5973
- disabled: disabled,
6148
+ children: [jsxRuntime.jsx(ToggleSwitch, {
5974
6149
  id: id,
5975
6150
  label: label,
5976
- onChange: setValue,
6151
+ value: value,
6152
+ onInput: setValue,
5977
6153
  onFocus: onFocus,
5978
6154
  onBlur: onBlur,
5979
- value: value,
6155
+ switcherLabel: switcherLabel,
6156
+ inline: inline,
5980
6157
  tooltip: tooltip,
5981
6158
  element: element
5982
- }, element), error && jsxRuntime.jsx("div", {
5983
- class: "bio-properties-panel-error",
5984
- children: error
5985
6159
  }), jsxRuntime.jsx(Description$1, {
5986
6160
  forId: id,
5987
6161
  element: element,
@@ -5998,254 +6172,38 @@ function isEdited$8(node) {
5998
6172
  function prefixId$7(id) {
5999
6173
  return `bio-properties-panel-${id}`;
6000
6174
  }
6001
- const useBufferedFocus$1 = function (editor, ref) {
6002
- const [buffer, setBuffer] = hooks.useState(undefined);
6003
- ref.current = hooks.useMemo(() => ({
6004
- focus: offset => {
6005
- if (editor) {
6006
- editor.focus(offset);
6007
- } else {
6008
- if (typeof offset === 'undefined') {
6009
- offset = Infinity;
6010
- }
6011
- setBuffer(offset);
6012
- }
6013
- }
6014
- }), [editor]);
6015
- hooks.useEffect(() => {
6016
- if (typeof buffer !== 'undefined' && editor) {
6017
- editor.focus(buffer);
6018
- setBuffer(false);
6019
- }
6020
- }, [editor, buffer]);
6021
- };
6022
- const CodeEditor$1 = React.forwardRef((props, ref) => {
6175
+ function NumberField(props) {
6023
6176
  const {
6024
- onInput,
6177
+ debounce,
6025
6178
  disabled,
6026
- tooltipContainer,
6027
- enableGutters,
6028
- value,
6029
- onLint = () => {},
6030
- contentAttributes = {},
6031
- hostLanguage = null,
6032
- singleLine = false
6179
+ displayLabel = true,
6180
+ id,
6181
+ inputRef,
6182
+ label,
6183
+ max,
6184
+ min,
6185
+ onInput,
6186
+ step,
6187
+ value = '',
6188
+ onFocus,
6189
+ onBlur
6033
6190
  } = props;
6034
- const inputRef = hooks.useRef();
6035
- const [editor, setEditor] = hooks.useState();
6036
- const [localValue, setLocalValue] = hooks.useState(value || '');
6037
- useBufferedFocus$1(editor, ref);
6038
- const handleInput = useStaticCallback(newValue => {
6039
- onInput(newValue);
6040
- setLocalValue(newValue);
6041
- });
6042
- hooks.useEffect(() => {
6043
- let editor;
6044
- editor = new feelers.FeelersEditor({
6045
- container: inputRef.current,
6046
- onChange: handleInput,
6047
- value: localValue,
6048
- onLint,
6049
- contentAttributes,
6050
- tooltipContainer,
6051
- enableGutters,
6052
- hostLanguage,
6053
- singleLine
6191
+ const [localValue, setLocalValue] = hooks.useState(value);
6192
+ const handleInputCallback = hooks.useMemo(() => {
6193
+ return debounce(event => {
6194
+ const {
6195
+ validity,
6196
+ value
6197
+ } = event.target;
6198
+ if (validity.valid) {
6199
+ onInput(value ? parseFloat(value) : undefined);
6200
+ }
6054
6201
  });
6055
- setEditor(editor);
6056
- return () => {
6057
- onLint([]);
6058
- inputRef.current.innerHTML = '';
6059
- setEditor(null);
6060
- };
6061
- }, []);
6062
- hooks.useEffect(() => {
6063
- if (!editor) {
6064
- return;
6065
- }
6066
- if (value === localValue) {
6067
- return;
6068
- }
6069
- editor.setValue(value);
6070
- setLocalValue(value);
6071
- }, [value]);
6072
- const handleClick = () => {
6073
- ref.current.focus();
6074
- };
6075
- return jsxRuntime.jsx("div", {
6076
- name: props.name,
6077
- class: classnames('bio-properties-panel-feelers-editor bio-properties-panel-input', localValue ? 'edited' : null, disabled ? 'disabled' : null),
6078
- ref: inputRef,
6079
- onClick: handleClick
6080
- });
6081
- });
6082
- const useBufferedFocus = function (editor, ref) {
6083
- const [buffer, setBuffer] = hooks.useState(undefined);
6084
- ref.current = hooks.useMemo(() => ({
6085
- focus: offset => {
6086
- if (editor) {
6087
- editor.focus(offset);
6088
- } else {
6089
- if (typeof offset === 'undefined') {
6090
- offset = Infinity;
6091
- }
6092
- setBuffer(offset);
6093
- }
6094
- }
6095
- }), [editor]);
6096
- hooks.useEffect(() => {
6097
- if (typeof buffer !== 'undefined' && editor) {
6098
- editor.focus(buffer);
6099
- setBuffer(false);
6100
- }
6101
- }, [editor, buffer]);
6102
- };
6103
- const CodeEditor = React.forwardRef((props, ref) => {
6104
- const {
6105
- value,
6106
- onInput,
6107
- onFeelToggle,
6108
- onLint = () => {},
6109
- disabled,
6110
- tooltipContainer,
6111
- variables
6112
- } = props;
6113
- const inputRef = hooks.useRef();
6114
- const [editor, setEditor] = hooks.useState();
6115
- const [localValue, setLocalValue] = hooks.useState(value || '');
6116
- useBufferedFocus(editor, ref);
6117
- const handleInput = useStaticCallback(newValue => {
6118
- onInput(newValue);
6119
- setLocalValue(newValue);
6120
- });
6121
- hooks.useEffect(() => {
6122
- let editor;
6123
-
6124
- /* Trigger FEEL toggle when
6125
- *
6126
- * - `backspace` is pressed
6127
- * - AND the cursor is at the beginning of the input
6128
- */
6129
- const onKeyDown = e => {
6130
- if (e.key !== 'Backspace' || !editor) {
6131
- return;
6132
- }
6133
- const selection = editor.getSelection();
6134
- const range = selection.ranges[selection.mainIndex];
6135
- if (range.from === 0 && range.to === 0) {
6136
- onFeelToggle();
6137
- }
6138
- };
6139
- editor = new FeelEditor({
6140
- container: inputRef.current,
6141
- onChange: handleInput,
6142
- onKeyDown: onKeyDown,
6143
- onLint: onLint,
6144
- tooltipContainer: tooltipContainer,
6145
- value: localValue,
6146
- variables: variables
6147
- });
6148
- setEditor(editor);
6149
- return () => {
6150
- onLint([]);
6151
- inputRef.current.innerHTML = '';
6152
- setEditor(null);
6153
- };
6154
- }, []);
6155
- hooks.useEffect(() => {
6156
- if (!editor) {
6157
- return;
6158
- }
6159
- if (value === localValue) {
6160
- return;
6161
- }
6162
- editor.setValue(value);
6163
- setLocalValue(value);
6164
- }, [value]);
6165
- hooks.useEffect(() => {
6166
- if (!editor) {
6167
- return;
6168
- }
6169
- editor.setVariables(variables);
6170
- }, [variables]);
6171
- const handleClick = () => {
6172
- ref.current.focus();
6173
- };
6174
- return jsxRuntime.jsx("div", {
6175
- class: classnames('bio-properties-panel-feel-editor-container', disabled ? 'disabled' : null),
6176
- children: jsxRuntime.jsx("div", {
6177
- name: props.name,
6178
- class: classnames('bio-properties-panel-input', localValue ? 'edited' : null),
6179
- ref: inputRef,
6180
- onClick: handleClick
6181
- })
6182
- });
6183
- });
6184
- function FeelIndicator(props) {
6185
- const {
6186
- active
6187
- } = props;
6188
- if (!active) {
6189
- return null;
6190
- }
6191
- return jsxRuntime.jsx("span", {
6192
- class: "bio-properties-panel-feel-indicator",
6193
- children: "="
6194
- });
6195
- }
6196
- const noop$2 = () => {};
6197
-
6198
- /**
6199
- * @param {Object} props
6200
- * @param {Object} props.label
6201
- * @param {String} props.feel
6202
- */
6203
- function FeelIcon(props) {
6204
- const {
6205
- feel = false,
6206
- active,
6207
- disabled = false,
6208
- onClick = noop$2
6209
- } = props;
6210
- const feelRequiredLabel = 'FEEL expression is mandatory';
6211
- const feelOptionalLabel = `Click to ${active ? 'remove' : 'set a'} dynamic value with FEEL expression`;
6212
- const handleClick = e => {
6213
- onClick(e);
6214
-
6215
- // when pointer event was created from keyboard, keep focus on button
6216
- if (!e.pointerType) {
6217
- e.stopPropagation();
6218
- }
6219
- };
6220
- return jsxRuntime.jsx("button", {
6221
- class: classnames('bio-properties-panel-feel-icon', active ? 'active' : null, feel === 'required' ? 'required' : 'optional'),
6222
- onClick: handleClick,
6223
- disabled: feel === 'required' || disabled,
6224
- title: feel === 'required' ? feelRequiredLabel : feelOptionalLabel,
6225
- children: jsxRuntime.jsx(FeelIcon$1, {})
6226
- });
6227
- }
6228
- function ToggleSwitch(props) {
6229
- const {
6230
- id,
6231
- label,
6232
- onInput,
6233
- value,
6234
- switcherLabel,
6235
- inline,
6236
- onFocus,
6237
- onBlur,
6238
- inputRef,
6239
- tooltip
6240
- } = props;
6241
- const [localValue, setLocalValue] = hooks.useState(value);
6242
- const handleInputCallback = async () => {
6243
- onInput(!value);
6244
- };
6245
- const handleInput = e => {
6246
- handleInputCallback();
6247
- setLocalValue(e.target.value);
6248
- };
6202
+ }, [onInput, debounce]);
6203
+ const handleInput = e => {
6204
+ handleInputCallback(e);
6205
+ setLocalValue(e.target.value);
6206
+ };
6249
6207
  hooks.useEffect(() => {
6250
6208
  if (value === localValue) {
6251
6209
  return;
@@ -6253,199 +6211,64 @@ function ToggleSwitch(props) {
6253
6211
  setLocalValue(value);
6254
6212
  }, [value]);
6255
6213
  return jsxRuntime.jsxs("div", {
6256
- class: classnames('bio-properties-panel-toggle-switch', {
6257
- inline
6258
- }),
6259
- children: [jsxRuntime.jsx("label", {
6260
- class: "bio-properties-panel-label",
6214
+ class: "bio-properties-panel-numberfield",
6215
+ children: [displayLabel && jsxRuntime.jsx("label", {
6261
6216
  for: prefixId$6(id),
6262
- children: jsxRuntime.jsx(TooltipWrapper, {
6263
- value: tooltip,
6264
- forId: id,
6265
- element: props.element,
6266
- children: label
6267
- })
6268
- }), jsxRuntime.jsxs("div", {
6269
- class: "bio-properties-panel-field-wrapper",
6270
- children: [jsxRuntime.jsxs("label", {
6271
- class: "bio-properties-panel-toggle-switch__switcher",
6272
- children: [jsxRuntime.jsx("input", {
6273
- ref: inputRef,
6274
- id: prefixId$6(id),
6275
- class: "bio-properties-panel-input",
6276
- type: "checkbox",
6277
- onFocus: onFocus,
6278
- onBlur: onBlur,
6279
- name: id,
6280
- onInput: handleInput,
6281
- checked: !!localValue
6282
- }), jsxRuntime.jsx("span", {
6283
- class: "bio-properties-panel-toggle-switch__slider"
6284
- })]
6285
- }), switcherLabel && jsxRuntime.jsx("p", {
6286
- class: "bio-properties-panel-toggle-switch__label",
6287
- children: switcherLabel
6288
- })]
6217
+ class: "bio-properties-panel-label",
6218
+ children: label
6219
+ }), jsxRuntime.jsx("input", {
6220
+ id: prefixId$6(id),
6221
+ ref: inputRef,
6222
+ type: "number",
6223
+ name: id,
6224
+ spellCheck: "false",
6225
+ autoComplete: "off",
6226
+ disabled: disabled,
6227
+ class: "bio-properties-panel-input",
6228
+ max: max,
6229
+ min: min,
6230
+ onInput: handleInput,
6231
+ onFocus: onFocus,
6232
+ onBlur: onBlur,
6233
+ step: step,
6234
+ value: localValue
6289
6235
  })]
6290
6236
  });
6291
6237
  }
6292
6238
 
6293
6239
  /**
6294
6240
  * @param {Object} props
6241
+ * @param {Boolean} props.debounce
6242
+ * @param {String} props.description
6243
+ * @param {Boolean} props.disabled
6295
6244
  * @param {Object} props.element
6245
+ * @param {Function} props.getValue
6296
6246
  * @param {String} props.id
6297
- * @param {String} props.description
6298
6247
  * @param {String} props.label
6299
- * @param {String} props.switcherLabel
6300
- * @param {Boolean} props.inline
6301
- * @param {Function} props.getValue
6248
+ * @param {String} props.max
6249
+ * @param {String} props.min
6302
6250
  * @param {Function} props.setValue
6303
6251
  * @param {Function} props.onFocus
6304
6252
  * @param {Function} props.onBlur
6305
- * @param {string|import('preact').Component} props.tooltip
6253
+ * @param {String} props.step
6254
+ * @param {Function} props.validate
6306
6255
  */
6307
- function ToggleSwitchEntry(props) {
6256
+ function NumberFieldEntry(props) {
6308
6257
  const {
6258
+ debounce,
6259
+ description,
6260
+ disabled,
6309
6261
  element,
6262
+ getValue,
6310
6263
  id,
6311
- description,
6312
6264
  label,
6313
- switcherLabel,
6314
- inline,
6315
- getValue,
6265
+ max,
6266
+ min,
6316
6267
  setValue,
6268
+ step,
6317
6269
  onFocus,
6318
6270
  onBlur,
6319
- tooltip
6320
- } = props;
6321
- const value = getValue(element);
6322
- return jsxRuntime.jsxs("div", {
6323
- class: "bio-properties-panel-entry bio-properties-panel-toggle-switch-entry",
6324
- "data-entry-id": id,
6325
- children: [jsxRuntime.jsx(ToggleSwitch, {
6326
- id: id,
6327
- label: label,
6328
- value: value,
6329
- onInput: setValue,
6330
- onFocus: onFocus,
6331
- onBlur: onBlur,
6332
- switcherLabel: switcherLabel,
6333
- inline: inline,
6334
- tooltip: tooltip,
6335
- element: element
6336
- }), jsxRuntime.jsx(Description$1, {
6337
- forId: id,
6338
- element: element,
6339
- value: description
6340
- })]
6341
- });
6342
- }
6343
- function isEdited$7(node) {
6344
- return node && !!node.checked;
6345
- }
6346
-
6347
- // helpers /////////////////
6348
-
6349
- function prefixId$6(id) {
6350
- return `bio-properties-panel-${id}`;
6351
- }
6352
- function NumberField(props) {
6353
- const {
6354
- debounce,
6355
- disabled,
6356
- displayLabel = true,
6357
- id,
6358
- inputRef,
6359
- label,
6360
- max,
6361
- min,
6362
- onInput,
6363
- step,
6364
- value = '',
6365
- onFocus,
6366
- onBlur
6367
- } = props;
6368
- const [localValue, setLocalValue] = hooks.useState(value);
6369
- const handleInputCallback = hooks.useMemo(() => {
6370
- return debounce(event => {
6371
- const {
6372
- validity,
6373
- value
6374
- } = event.target;
6375
- if (validity.valid) {
6376
- onInput(value ? parseFloat(value) : undefined);
6377
- }
6378
- });
6379
- }, [onInput, debounce]);
6380
- const handleInput = e => {
6381
- handleInputCallback(e);
6382
- setLocalValue(e.target.value);
6383
- };
6384
- hooks.useEffect(() => {
6385
- if (value === localValue) {
6386
- return;
6387
- }
6388
- setLocalValue(value);
6389
- }, [value]);
6390
- return jsxRuntime.jsxs("div", {
6391
- class: "bio-properties-panel-numberfield",
6392
- children: [displayLabel && jsxRuntime.jsx("label", {
6393
- for: prefixId$5(id),
6394
- class: "bio-properties-panel-label",
6395
- children: label
6396
- }), jsxRuntime.jsx("input", {
6397
- id: prefixId$5(id),
6398
- ref: inputRef,
6399
- type: "number",
6400
- name: id,
6401
- spellCheck: "false",
6402
- autoComplete: "off",
6403
- disabled: disabled,
6404
- class: "bio-properties-panel-input",
6405
- max: max,
6406
- min: min,
6407
- onInput: handleInput,
6408
- onFocus: onFocus,
6409
- onBlur: onBlur,
6410
- step: step,
6411
- value: localValue
6412
- })]
6413
- });
6414
- }
6415
-
6416
- /**
6417
- * @param {Object} props
6418
- * @param {Boolean} props.debounce
6419
- * @param {String} props.description
6420
- * @param {Boolean} props.disabled
6421
- * @param {Object} props.element
6422
- * @param {Function} props.getValue
6423
- * @param {String} props.id
6424
- * @param {String} props.label
6425
- * @param {String} props.max
6426
- * @param {String} props.min
6427
- * @param {Function} props.setValue
6428
- * @param {Function} props.onFocus
6429
- * @param {Function} props.onBlur
6430
- * @param {String} props.step
6431
- * @param {Function} props.validate
6432
- */
6433
- function NumberFieldEntry(props) {
6434
- const {
6435
- debounce,
6436
- description,
6437
- disabled,
6438
- element,
6439
- getValue,
6440
- id,
6441
- label,
6442
- max,
6443
- min,
6444
- setValue,
6445
- step,
6446
- onFocus,
6447
- onBlur,
6448
- validate
6271
+ validate
6449
6272
  } = props;
6450
6273
  const globalError = useError(id);
6451
6274
  const [localError, setLocalError] = hooks.useState(null);
@@ -6490,27 +6313,30 @@ function NumberFieldEntry(props) {
6490
6313
  })]
6491
6314
  });
6492
6315
  }
6493
- function isEdited$6(node) {
6316
+ function isEdited$7(node) {
6494
6317
  return node && !!node.value;
6495
6318
  }
6496
6319
 
6497
6320
  // helpers /////////////////
6498
6321
 
6499
- function prefixId$5(id) {
6322
+ function prefixId$6(id) {
6500
6323
  return `bio-properties-panel-${id}`;
6501
6324
  }
6502
- const noop$1 = () => {};
6325
+ const noop$2 = () => {};
6503
6326
  function FeelTextfield(props) {
6504
6327
  const {
6505
6328
  debounce,
6506
6329
  id,
6330
+ element,
6507
6331
  label,
6332
+ hostLanguage,
6508
6333
  onInput,
6509
6334
  onError,
6510
6335
  feel,
6511
6336
  value = '',
6512
6337
  disabled = false,
6513
6338
  variables,
6339
+ singleLine,
6514
6340
  tooltipContainer,
6515
6341
  OptionalComponent = OptionalFeelInput,
6516
6342
  tooltip
@@ -6521,6 +6347,11 @@ function FeelTextfield(props) {
6521
6347
  const feelActive = minDash.isString(localValue) && localValue.startsWith('=') || feel === 'required';
6522
6348
  const feelOnlyValue = minDash.isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
6523
6349
  const [focus, _setFocus] = hooks.useState(undefined);
6350
+ const {
6351
+ open: openPopup,
6352
+ source: popupSource
6353
+ } = hooks.useContext(FeelPopupContext);
6354
+ const popuOpen = popupSource === id;
6524
6355
  const setFocus = (offset = 0) => {
6525
6356
  const hasFocus = containerRef.current.contains(document.activeElement);
6526
6357
 
@@ -6573,6 +6404,21 @@ function FeelTextfield(props) {
6573
6404
  const message = `${error.source}: ${error.message}`;
6574
6405
  onError(message);
6575
6406
  });
6407
+ const handlePopupOpen = (type = 'feel') => {
6408
+ const popupOptions = {
6409
+ id,
6410
+ hostLanguage,
6411
+ onInput: handleLocalInput,
6412
+ position: calculatePopupPosition(containerRef.current),
6413
+ singleLine,
6414
+ title: getPopupTitle(element, label),
6415
+ tooltipContainer,
6416
+ type,
6417
+ value: feelOnlyValue,
6418
+ variables
6419
+ };
6420
+ openPopup(id, popupOptions, editorRef.current);
6421
+ };
6576
6422
  hooks.useEffect(() => {
6577
6423
  if (typeof focus !== 'undefined') {
6578
6424
  editorRef.current.focus(focus);
@@ -6601,7 +6447,7 @@ function FeelTextfield(props) {
6601
6447
  event.clipboardData.setData('application/FEEL', event.clipboardData.getData('text'));
6602
6448
  };
6603
6449
  const pasteHandler = event => {
6604
- if (feelActive) {
6450
+ if (feelActive || popuOpen) {
6605
6451
  return;
6606
6452
  }
6607
6453
  const data = event.clipboardData.getData('application/FEEL');
@@ -6626,7 +6472,7 @@ function FeelTextfield(props) {
6626
6472
  'feel-active': feelActive
6627
6473
  }),
6628
6474
  children: [jsxRuntime.jsxs("label", {
6629
- for: prefixId$4(id),
6475
+ for: prefixId$5(id),
6630
6476
  class: "bio-properties-panel-label",
6631
6477
  onClick: () => setFocus(),
6632
6478
  children: [jsxRuntime.jsx(TooltipWrapper, {
@@ -6648,28 +6494,33 @@ function FeelTextfield(props) {
6648
6494
  disabled: feel !== 'optional' || disabled,
6649
6495
  onClick: handleFeelToggle
6650
6496
  }), feelActive ? jsxRuntime.jsx(CodeEditor, {
6651
- id: prefixId$4(id),
6497
+ id: prefixId$5(id),
6652
6498
  name: id,
6653
6499
  onInput: handleLocalInput,
6654
6500
  disabled: disabled,
6501
+ popupOpen: popuOpen,
6655
6502
  onFeelToggle: () => {
6656
6503
  handleFeelToggle();
6657
6504
  setFocus(true);
6658
6505
  },
6659
6506
  onLint: handleLint,
6507
+ onPopupOpen: handlePopupOpen,
6660
6508
  value: feelOnlyValue,
6661
6509
  variables: variables,
6662
6510
  ref: editorRef,
6663
6511
  tooltipContainer: tooltipContainer
6664
6512
  }) : jsxRuntime.jsx(OptionalComponent, {
6665
6513
  ...props,
6514
+ popupOpen: popuOpen,
6666
6515
  onInput: handleLocalInput,
6667
6516
  contentAttributes: {
6668
- 'id': prefixId$4(id),
6517
+ 'id': prefixId$5(id),
6669
6518
  'aria-label': label
6670
6519
  },
6671
6520
  value: localValue,
6672
- ref: editorRef
6521
+ ref: editorRef,
6522
+ onPopupOpen: handlePopupOpen,
6523
+ containerRef: containerRef
6673
6524
  })]
6674
6525
  })]
6675
6526
  });
@@ -6703,7 +6554,7 @@ const OptionalFeelInput = React.forwardRef((props, ref) => {
6703
6554
  }
6704
6555
  };
6705
6556
  return jsxRuntime.jsx("input", {
6706
- id: prefixId$4(id),
6557
+ id: prefixId$5(id),
6707
6558
  type: "text",
6708
6559
  ref: inputRef,
6709
6560
  name: id,
@@ -6730,309 +6581,995 @@ const OptionalFeelNumberField = React.forwardRef((props, ref) => {
6730
6581
  onFocus,
6731
6582
  onBlur
6732
6583
  } = props;
6733
- const inputRef = hooks.useRef();
6584
+ const inputRef = hooks.useRef();
6585
+
6586
+ // To be consistent with the FEEL editor, set focus at start of input
6587
+ // this ensures clean editing experience when switching with the keyboard
6588
+ ref.current = {
6589
+ focus: position => {
6590
+ const input = inputRef.current;
6591
+ if (!input) {
6592
+ return;
6593
+ }
6594
+ input.focus();
6595
+ if (typeof position === 'number' && position !== Infinity) {
6596
+ if (position > value.length) {
6597
+ position = value.length;
6598
+ }
6599
+ input.setSelectionRange(position, position);
6600
+ }
6601
+ }
6602
+ };
6603
+ return jsxRuntime.jsx(NumberField, {
6604
+ id: id,
6605
+ debounce: debounce,
6606
+ disabled: disabled,
6607
+ displayLabel: false,
6608
+ inputRef: inputRef,
6609
+ max: max,
6610
+ min: min,
6611
+ onInput: onInput,
6612
+ step: step,
6613
+ value: value,
6614
+ onFocus: onFocus,
6615
+ onBlur: onBlur
6616
+ });
6617
+ });
6618
+ React.forwardRef((props, ref) => {
6619
+ const {
6620
+ id,
6621
+ disabled,
6622
+ onInput,
6623
+ value,
6624
+ onFocus,
6625
+ onBlur
6626
+ } = props;
6627
+ const inputRef = hooks.useRef();
6628
+
6629
+ // To be consistent with the FEEL editor, set focus at start of input
6630
+ // this ensures clean editing experience when switching with the keyboard
6631
+ ref.current = {
6632
+ focus: () => {
6633
+ const input = inputRef.current;
6634
+ if (!input) {
6635
+ return;
6636
+ }
6637
+ input.focus();
6638
+ input.setSelectionRange(0, 0);
6639
+ }
6640
+ };
6641
+ return jsxRuntime.jsx("textarea", {
6642
+ id: prefixId$5(id),
6643
+ type: "text",
6644
+ ref: inputRef,
6645
+ name: id,
6646
+ spellCheck: "false",
6647
+ autoComplete: "off",
6648
+ disabled: disabled,
6649
+ class: "bio-properties-panel-input",
6650
+ onInput: e => onInput(e.target.value),
6651
+ onFocus: onFocus,
6652
+ onBlur: onBlur,
6653
+ value: value || '',
6654
+ "data-gramm": "false"
6655
+ });
6656
+ });
6657
+ const OptionalFeelToggleSwitch = React.forwardRef((props, ref) => {
6658
+ const {
6659
+ id,
6660
+ onInput,
6661
+ value,
6662
+ onFocus,
6663
+ onBlur,
6664
+ switcherLabel
6665
+ } = props;
6666
+ const inputRef = hooks.useRef();
6667
+
6668
+ // To be consistent with the FEEL editor, set focus at start of input
6669
+ // this ensures clean editing experience when switching with the keyboard
6670
+ ref.current = {
6671
+ focus: () => {
6672
+ const input = inputRef.current;
6673
+ if (!input) {
6674
+ return;
6675
+ }
6676
+ input.focus();
6677
+ }
6678
+ };
6679
+ return jsxRuntime.jsx(ToggleSwitch, {
6680
+ id: id,
6681
+ value: value,
6682
+ inputRef: inputRef,
6683
+ onInput: onInput,
6684
+ onFocus: onFocus,
6685
+ onBlur: onBlur,
6686
+ switcherLabel: switcherLabel
6687
+ });
6688
+ });
6689
+ React.forwardRef((props, ref) => {
6690
+ const {
6691
+ id,
6692
+ disabled,
6693
+ onInput,
6694
+ value,
6695
+ onFocus,
6696
+ onBlur
6697
+ } = props;
6698
+ const inputRef = hooks.useRef();
6699
+ const handleChange = ({
6700
+ target
6701
+ }) => {
6702
+ onInput(target.checked);
6703
+ };
6704
+
6705
+ // To be consistent with the FEEL editor, set focus at start of input
6706
+ // this ensures clean editing experience when switching with the keyboard
6707
+ ref.current = {
6708
+ focus: () => {
6709
+ const input = inputRef.current;
6710
+ if (!input) {
6711
+ return;
6712
+ }
6713
+ input.focus();
6714
+ }
6715
+ };
6716
+ return jsxRuntime.jsx("input", {
6717
+ ref: inputRef,
6718
+ id: prefixId$5(id),
6719
+ name: id,
6720
+ onFocus: onFocus,
6721
+ onBlur: onBlur,
6722
+ type: "checkbox",
6723
+ class: "bio-properties-panel-input",
6724
+ onChange: handleChange,
6725
+ checked: value,
6726
+ disabled: disabled
6727
+ });
6728
+ });
6729
+
6730
+ /**
6731
+ * @param {Object} props
6732
+ * @param {Object} props.element
6733
+ * @param {String} props.id
6734
+ * @param {String} props.description
6735
+ * @param {Boolean} props.debounce
6736
+ * @param {Boolean} props.disabled
6737
+ * @param {Boolean} props.feel
6738
+ * @param {String} props.label
6739
+ * @param {Function} props.getValue
6740
+ * @param {Function} props.setValue
6741
+ * @param {Function} props.tooltipContainer
6742
+ * @param {Function} props.validate
6743
+ * @param {Function} props.show
6744
+ * @param {Function} props.example
6745
+ * @param {Function} props.variables
6746
+ * @param {Function} props.onFocus
6747
+ * @param {Function} props.onBlur
6748
+ * @param {string|import('preact').Component} props.tooltip
6749
+ */
6750
+ function FeelEntry(props) {
6751
+ const {
6752
+ element,
6753
+ id,
6754
+ description,
6755
+ debounce,
6756
+ disabled,
6757
+ feel,
6758
+ label,
6759
+ getValue,
6760
+ setValue,
6761
+ tooltipContainer,
6762
+ hostLanguage,
6763
+ singleLine,
6764
+ validate,
6765
+ show = noop$2,
6766
+ example,
6767
+ variables,
6768
+ onFocus,
6769
+ onBlur,
6770
+ tooltip
6771
+ } = props;
6772
+ const [validationError, setValidationError] = hooks.useState(null);
6773
+ const [localError, setLocalError] = hooks.useState(null);
6774
+ let value = getValue(element);
6775
+ hooks.useEffect(() => {
6776
+ if (minDash.isFunction(validate)) {
6777
+ const newValidationError = validate(value) || null;
6778
+ setValidationError(newValidationError);
6779
+ }
6780
+ }, [value]);
6781
+ const onInput = useStaticCallback(newValue => {
6782
+ let newValidationError = null;
6783
+ if (minDash.isFunction(validate)) {
6784
+ newValidationError = validate(newValue) || null;
6785
+ }
6786
+
6787
+ // don't create multiple commandStack entries for the same value
6788
+ if (newValue !== value) {
6789
+ setValue(newValue, newValidationError);
6790
+ }
6791
+ setValidationError(newValidationError);
6792
+ });
6793
+ const onError = hooks.useCallback(err => {
6794
+ setLocalError(err);
6795
+ }, []);
6796
+ const temporaryError = useError(id);
6797
+ const error = localError || temporaryError || validationError;
6798
+ return jsxRuntime.jsxs("div", {
6799
+ class: classnames(props.class, 'bio-properties-panel-entry', error ? 'has-error' : ''),
6800
+ "data-entry-id": id,
6801
+ children: [preact.createElement(FeelTextfield, {
6802
+ ...props,
6803
+ debounce: debounce,
6804
+ disabled: disabled,
6805
+ feel: feel,
6806
+ id: id,
6807
+ key: element,
6808
+ label: label,
6809
+ onInput: onInput,
6810
+ onError: onError,
6811
+ onFocus: onFocus,
6812
+ onBlur: onBlur,
6813
+ example: example,
6814
+ hostLanguage: hostLanguage,
6815
+ singleLine: singleLine,
6816
+ show: show,
6817
+ value: value,
6818
+ variables: variables,
6819
+ tooltipContainer: tooltipContainer,
6820
+ OptionalComponent: props.OptionalComponent,
6821
+ tooltip: tooltip
6822
+ }), error && jsxRuntime.jsx("div", {
6823
+ class: "bio-properties-panel-error",
6824
+ children: error
6825
+ }), jsxRuntime.jsx(Description$1, {
6826
+ forId: id,
6827
+ element: element,
6828
+ value: description
6829
+ })]
6830
+ });
6831
+ }
6832
+
6833
+ /**
6834
+ * @param {Object} props
6835
+ * @param {Object} props.element
6836
+ * @param {String} props.id
6837
+ * @param {String} props.description
6838
+ * @param {Boolean} props.debounce
6839
+ * @param {Boolean} props.disabled
6840
+ * @param {String} props.max
6841
+ * @param {String} props.min
6842
+ * @param {String} props.step
6843
+ * @param {Boolean} props.feel
6844
+ * @param {String} props.label
6845
+ * @param {Function} props.getValue
6846
+ * @param {Function} props.setValue
6847
+ * @param {Function} props.tooltipContainer
6848
+ * @param {Function} props.validate
6849
+ * @param {Function} props.show
6850
+ * @param {Function} props.example
6851
+ * @param {Function} props.variables
6852
+ * @param {Function} props.onFocus
6853
+ * @param {Function} props.onBlur
6854
+ */
6855
+ function FeelNumberEntry(props) {
6856
+ return jsxRuntime.jsx(FeelEntry, {
6857
+ class: "bio-properties-panel-feel-number",
6858
+ OptionalComponent: OptionalFeelNumberField,
6859
+ ...props
6860
+ });
6861
+ }
6862
+
6863
+ /**
6864
+ * @param {Object} props
6865
+ * @param {Object} props.element
6866
+ * @param {String} props.id
6867
+ * @param {String} props.description
6868
+ * @param {Boolean} props.debounce
6869
+ * @param {Boolean} props.disabled
6870
+ * @param {Boolean} props.feel
6871
+ * @param {String} props.label
6872
+ * @param {Function} props.getValue
6873
+ * @param {Function} props.setValue
6874
+ * @param {Function} props.tooltipContainer
6875
+ * @param {Function} props.validate
6876
+ * @param {Function} props.show
6877
+ * @param {Function} props.example
6878
+ * @param {Function} props.variables
6879
+ * @param {Function} props.onFocus
6880
+ * @param {Function} props.onBlur
6881
+ */
6882
+ function FeelToggleSwitchEntry(props) {
6883
+ return jsxRuntime.jsx(FeelEntry, {
6884
+ class: "bio-properties-panel-feel-toggle-switch",
6885
+ OptionalComponent: OptionalFeelToggleSwitch,
6886
+ ...props
6887
+ });
6888
+ }
6889
+
6890
+ /**
6891
+ * @param {Object} props
6892
+ * @param {Object} props.element
6893
+ * @param {String} props.id
6894
+ * @param {String} props.description
6895
+ * @param {String} props.hostLanguage
6896
+ * @param {Boolean} props.singleLine
6897
+ * @param {Boolean} props.debounce
6898
+ * @param {Boolean} props.disabled
6899
+ * @param {Boolean} props.feel
6900
+ * @param {String} props.label
6901
+ * @param {Function} props.getValue
6902
+ * @param {Function} props.setValue
6903
+ * @param {Function} props.tooltipContainer
6904
+ * @param {Function} props.validate
6905
+ * @param {Function} props.show
6906
+ * @param {Function} props.example
6907
+ * @param {Function} props.variables
6908
+ * @param {Function} props.onFocus
6909
+ * @param {Function} props.onBlur
6910
+ */
6911
+ function FeelTemplatingEntry(props) {
6912
+ return jsxRuntime.jsx(FeelEntry, {
6913
+ class: "bio-properties-panel-feel-templating",
6914
+ OptionalComponent: CodeEditor$1,
6915
+ ...props
6916
+ });
6917
+ }
6918
+ function isEdited$6(node) {
6919
+ if (!node) {
6920
+ return false;
6921
+ }
6922
+ if (node.type === 'checkbox') {
6923
+ return !!node.checked || node.classList.contains('edited');
6924
+ }
6925
+ return !!node.value || node.classList.contains('edited');
6926
+ }
6927
+
6928
+ // helpers /////////////////
6929
+
6930
+ function prefixId$5(id) {
6931
+ return `bio-properties-panel-${id}`;
6932
+ }
6933
+ function calculatePopupPosition(element) {
6934
+ const {
6935
+ top,
6936
+ left
6937
+ } = element.getBoundingClientRect();
6938
+ return {
6939
+ left: left - FEEL_POPUP_WIDTH - 20,
6940
+ top: top
6941
+ };
6942
+ }
6943
+
6944
+ // todo(pinussilvestrus): make this configurable in the future
6945
+ function getPopupTitle(element, label) {
6946
+ let popupTitle;
6947
+ if (element && element.type) {
6948
+ popupTitle = `${element.type} / `;
6949
+ }
6950
+ return `${popupTitle}${label}`;
6951
+ }
6952
+ const DEFAULT_LAYOUT = {};
6953
+ const DEFAULT_DESCRIPTION = {};
6954
+ const DEFAULT_TOOLTIP = {};
6955
+
6956
+ /**
6957
+ * @typedef { {
6958
+ * component: import('preact').Component,
6959
+ * id: String,
6960
+ * isEdited?: Function
6961
+ * } } EntryDefinition
6962
+ *
6963
+ * @typedef { {
6964
+ * autoFocusEntry: String,
6965
+ * autoOpen?: Boolean,
6966
+ * entries: Array<EntryDefinition>,
6967
+ * id: String,
6968
+ * label: String,
6969
+ * remove: (event: MouseEvent) => void
6970
+ * } } ListItemDefinition
6971
+ *
6972
+ * @typedef { {
6973
+ * add: (event: MouseEvent) => void,
6974
+ * component: import('preact').Component,
6975
+ * element: Object,
6976
+ * id: String,
6977
+ * items: Array<ListItemDefinition>,
6978
+ * label: String,
6979
+ * shouldSort?: Boolean,
6980
+ * shouldOpen?: Boolean
6981
+ * } } ListGroupDefinition
6982
+ *
6983
+ * @typedef { {
6984
+ * component?: import('preact').Component,
6985
+ * entries: Array<EntryDefinition>,
6986
+ * id: String,
6987
+ * label: String,
6988
+ * shouldOpen?: Boolean
6989
+ * } } GroupDefinition
6990
+ *
6991
+ * @typedef { {
6992
+ * [id: String]: GetDescriptionFunction
6993
+ * } } DescriptionConfig
6994
+ *
6995
+ * @typedef { {
6996
+ * [id: String]: GetTooltipFunction
6997
+ * } } TooltipConfig
6998
+ *
6999
+ * @callback { {
7000
+ * @param {string} id
7001
+ * @param {Object} element
7002
+ * @returns {string}
7003
+ * } } GetDescriptionFunction
7004
+ *
7005
+ * @callback { {
7006
+ * @param {string} id
7007
+ * @param {Object} element
7008
+ * @returns {string}
7009
+ * } } GetTooltipFunction
7010
+ *
7011
+ * @typedef { {
7012
+ * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
7013
+ * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
7014
+ * } } PlaceholderProvider
7015
+ *
7016
+ */
7017
+
7018
+ /**
7019
+ * A basic properties panel component. Describes *how* content will be rendered, accepts
7020
+ * data from implementor to describe *what* will be rendered.
7021
+ *
7022
+ * @param {Object} props
7023
+ * @param {Object|Array} props.element
7024
+ * @param {import('./components/Header').HeaderProvider} props.headerProvider
7025
+ * @param {PlaceholderProvider} [props.placeholderProvider]
7026
+ * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
7027
+ * @param {Object} [props.layoutConfig]
7028
+ * @param {Function} [props.layoutChanged]
7029
+ * @param {DescriptionConfig} [props.descriptionConfig]
7030
+ * @param {Function} [props.descriptionLoaded]
7031
+ * @param {TooltipConfig} [props.tooltipConfig]
7032
+ * @param {Function} [props.tooltipLoaded]
7033
+ * @param {Object} [props.eventBus]
7034
+ */
7035
+ function PropertiesPanel(props) {
7036
+ const {
7037
+ element,
7038
+ headerProvider,
7039
+ placeholderProvider,
7040
+ groups,
7041
+ layoutConfig,
7042
+ layoutChanged,
7043
+ descriptionConfig,
7044
+ descriptionLoaded,
7045
+ tooltipConfig,
7046
+ tooltipLoaded,
7047
+ eventBus
7048
+ } = props;
7049
+
7050
+ // set-up layout context
7051
+ const [layout, setLayout] = hooks.useState(createLayout(layoutConfig));
7052
+
7053
+ // react to external changes in the layout config
7054
+ useUpdateLayoutEffect(() => {
7055
+ const newLayout = createLayout(layoutConfig);
7056
+ setLayout(newLayout);
7057
+ }, [layoutConfig]);
7058
+ hooks.useEffect(() => {
7059
+ if (typeof layoutChanged === 'function') {
7060
+ layoutChanged(layout);
7061
+ }
7062
+ }, [layout, layoutChanged]);
7063
+ const getLayoutForKey = (key, defaultValue) => {
7064
+ return minDash.get(layout, key, defaultValue);
7065
+ };
7066
+ const setLayoutForKey = (key, config) => {
7067
+ const newLayout = minDash.assign({}, layout);
7068
+ minDash.set(newLayout, key, config);
7069
+ setLayout(newLayout);
7070
+ };
7071
+ const layoutContext = {
7072
+ layout,
7073
+ setLayout,
7074
+ getLayoutForKey,
7075
+ setLayoutForKey
7076
+ };
7077
+
7078
+ // set-up description context
7079
+ const description = hooks.useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
7080
+ hooks.useEffect(() => {
7081
+ if (typeof descriptionLoaded === 'function') {
7082
+ descriptionLoaded(description);
7083
+ }
7084
+ }, [description, descriptionLoaded]);
7085
+ const getDescriptionForId = (id, element) => {
7086
+ return description[id] && description[id](element);
7087
+ };
7088
+ const descriptionContext = {
7089
+ description,
7090
+ getDescriptionForId
7091
+ };
7092
+
7093
+ // set-up tooltip context
7094
+ const tooltip = hooks.useMemo(() => createTooltipContext(tooltipConfig), [tooltipConfig]);
7095
+ hooks.useEffect(() => {
7096
+ if (typeof tooltipLoaded === 'function') {
7097
+ tooltipLoaded(tooltip);
7098
+ }
7099
+ }, [tooltip, tooltipLoaded]);
7100
+ const getTooltipForId = (id, element) => {
7101
+ return tooltip[id] && tooltip[id](element);
7102
+ };
7103
+ const tooltipContext = {
7104
+ tooltip,
7105
+ getTooltipForId
7106
+ };
7107
+ const [errors, setErrors] = hooks.useState({});
7108
+ const onSetErrors = ({
7109
+ errors
7110
+ }) => setErrors(errors);
7111
+ useEvent('propertiesPanel.setErrors', onSetErrors, eventBus);
7112
+ const errorsContext = {
7113
+ errors
7114
+ };
7115
+ const eventContext = {
7116
+ eventBus
7117
+ };
7118
+ const propertiesPanelContext = {
7119
+ element
7120
+ };
7121
+
7122
+ // empty state
7123
+ if (placeholderProvider && !element) {
7124
+ return jsxRuntime.jsx(Placeholder, {
7125
+ ...placeholderProvider.getEmpty()
7126
+ });
7127
+ }
7128
+
7129
+ // multiple state
7130
+ if (placeholderProvider && minDash.isArray(element)) {
7131
+ return jsxRuntime.jsx(Placeholder, {
7132
+ ...placeholderProvider.getMultiple()
7133
+ });
7134
+ }
7135
+ return jsxRuntime.jsx(LayoutContext.Provider, {
7136
+ value: propertiesPanelContext,
7137
+ children: jsxRuntime.jsx(ErrorsContext.Provider, {
7138
+ value: errorsContext,
7139
+ children: jsxRuntime.jsx(DescriptionContext.Provider, {
7140
+ value: descriptionContext,
7141
+ children: jsxRuntime.jsx(TooltipContext.Provider, {
7142
+ value: tooltipContext,
7143
+ children: jsxRuntime.jsx(LayoutContext.Provider, {
7144
+ value: layoutContext,
7145
+ children: jsxRuntime.jsx(EventContext.Provider, {
7146
+ value: eventContext,
7147
+ children: jsxRuntime.jsx(FEELPopupRoot, {
7148
+ element: element,
7149
+ children: jsxRuntime.jsxs("div", {
7150
+ class: "bio-properties-panel",
7151
+ children: [jsxRuntime.jsx(Header, {
7152
+ element: element,
7153
+ headerProvider: headerProvider
7154
+ }), jsxRuntime.jsx("div", {
7155
+ class: "bio-properties-panel-scroll-container",
7156
+ children: groups.map(group => {
7157
+ const {
7158
+ component: Component = Group,
7159
+ id
7160
+ } = group;
7161
+ return preact.createElement(Component, {
7162
+ ...group,
7163
+ key: id,
7164
+ element: element
7165
+ });
7166
+ })
7167
+ })]
7168
+ })
7169
+ })
7170
+ })
7171
+ })
7172
+ })
7173
+ })
7174
+ })
7175
+ });
7176
+ }
7177
+
7178
+ // helpers //////////////////
7179
+
7180
+ function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
7181
+ return {
7182
+ ...defaults,
7183
+ ...overrides
7184
+ };
7185
+ }
7186
+ function createDescriptionContext(overrides = {}) {
7187
+ return {
7188
+ ...DEFAULT_DESCRIPTION,
7189
+ ...overrides
7190
+ };
7191
+ }
7192
+ function createTooltipContext(overrides = {}) {
7193
+ return {
7194
+ ...DEFAULT_TOOLTIP,
7195
+ ...overrides
7196
+ };
7197
+ }
7198
+
7199
+ // hooks //////////////////
7200
+
7201
+ /**
7202
+ * This hook behaves like useLayoutEffect, but does not trigger on the first render.
7203
+ *
7204
+ * @param {Function} effect
7205
+ * @param {Array} deps
7206
+ */
7207
+ function useUpdateLayoutEffect(effect, deps) {
7208
+ const isMounted = hooks.useRef(false);
7209
+ hooks.useLayoutEffect(() => {
7210
+ if (isMounted.current) {
7211
+ return effect();
7212
+ } else {
7213
+ isMounted.current = true;
7214
+ }
7215
+ }, deps);
7216
+ }
7217
+ function CollapsibleEntry(props) {
7218
+ const {
7219
+ element,
7220
+ entries = [],
7221
+ id,
7222
+ label,
7223
+ open: shouldOpen,
7224
+ remove
7225
+ } = props;
7226
+ const [open, setOpen] = hooks.useState(shouldOpen);
7227
+ const toggleOpen = () => setOpen(!open);
7228
+ const {
7229
+ onShow
7230
+ } = hooks.useContext(LayoutContext);
7231
+ const propertiesPanelContext = {
7232
+ ...hooks.useContext(LayoutContext),
7233
+ onShow: hooks.useCallback(() => {
7234
+ setOpen(true);
7235
+ if (minDash.isFunction(onShow)) {
7236
+ onShow();
7237
+ }
7238
+ }, [onShow, setOpen])
7239
+ };
7240
+
7241
+ // todo(pinussilvestrus): translate once we have a translate mechanism for the core
7242
+ const placeholderLabel = '<empty>';
7243
+ return jsxRuntime.jsxs("div", {
7244
+ "data-entry-id": id,
7245
+ class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
7246
+ children: [jsxRuntime.jsxs("div", {
7247
+ class: "bio-properties-panel-collapsible-entry-header",
7248
+ onClick: toggleOpen,
7249
+ children: [jsxRuntime.jsx("div", {
7250
+ title: label || placeholderLabel,
7251
+ class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
7252
+ children: label || placeholderLabel
7253
+ }), jsxRuntime.jsx("button", {
7254
+ title: "Toggle list item",
7255
+ class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
7256
+ children: jsxRuntime.jsx(ArrowIcon, {
7257
+ class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
7258
+ })
7259
+ }), remove ? jsxRuntime.jsx("button", {
7260
+ title: "Delete item",
7261
+ class: "bio-properties-panel-remove-entry",
7262
+ onClick: remove,
7263
+ children: jsxRuntime.jsx(DeleteIcon, {})
7264
+ }) : null]
7265
+ }), jsxRuntime.jsx("div", {
7266
+ class: classnames('bio-properties-panel-collapsible-entry-entries', open ? 'open' : ''),
7267
+ children: jsxRuntime.jsx(LayoutContext.Provider, {
7268
+ value: propertiesPanelContext,
7269
+ children: entries.map(entry => {
7270
+ const {
7271
+ component: Component,
7272
+ id
7273
+ } = entry;
7274
+ return preact.createElement(Component, {
7275
+ ...entry,
7276
+ element: element,
7277
+ key: id
7278
+ });
7279
+ })
7280
+ })
7281
+ })]
7282
+ });
7283
+ }
7284
+ function ListItem(props) {
7285
+ const {
7286
+ autoFocusEntry,
7287
+ autoOpen
7288
+ } = props;
6734
7289
 
6735
- // To be consistent with the FEEL editor, set focus at start of input
6736
- // this ensures clean editing experience when switching with the keyboard
6737
- ref.current = {
6738
- focus: position => {
6739
- const input = inputRef.current;
6740
- if (!input) {
6741
- return;
6742
- }
6743
- input.focus();
6744
- if (typeof position === 'number' && position !== Infinity) {
6745
- if (position > value.length) {
6746
- position = value.length;
7290
+ // focus specified entry on auto open
7291
+ hooks.useEffect(() => {
7292
+ if (autoOpen && autoFocusEntry) {
7293
+ const entry = minDom.query(`[data-entry-id="${autoFocusEntry}"]`);
7294
+ const focusableInput = minDom.query('.bio-properties-panel-input', entry);
7295
+ if (focusableInput) {
7296
+ if (minDash.isFunction(focusableInput.select)) {
7297
+ focusableInput.select();
7298
+ } else if (minDash.isFunction(focusableInput.focus)) {
7299
+ focusableInput.focus();
6747
7300
  }
6748
- input.setSelectionRange(position, position);
6749
7301
  }
6750
7302
  }
6751
- };
6752
- return jsxRuntime.jsx(NumberField, {
6753
- id: id,
6754
- debounce: debounce,
6755
- disabled: disabled,
6756
- displayLabel: false,
6757
- inputRef: inputRef,
6758
- max: max,
6759
- min: min,
6760
- onInput: onInput,
6761
- step: step,
6762
- value: value,
6763
- onFocus: onFocus,
6764
- onBlur: onBlur
7303
+ }, [autoOpen, autoFocusEntry]);
7304
+ return jsxRuntime.jsx("div", {
7305
+ class: "bio-properties-panel-list-item",
7306
+ children: jsxRuntime.jsx(CollapsibleEntry, {
7307
+ ...props,
7308
+ open: autoOpen
7309
+ })
6765
7310
  });
6766
- });
6767
- React.forwardRef((props, ref) => {
7311
+ }
7312
+ const noop$1 = () => {};
7313
+
7314
+ /**
7315
+ * @param {import('../PropertiesPanel').ListGroupDefinition} props
7316
+ */
7317
+ function ListGroup(props) {
6768
7318
  const {
7319
+ add,
7320
+ element,
6769
7321
  id,
6770
- disabled,
6771
- onInput,
6772
- value,
6773
- onFocus,
6774
- onBlur
7322
+ items,
7323
+ label,
7324
+ shouldOpen = true,
7325
+ shouldSort = true
6775
7326
  } = props;
6776
- const inputRef = hooks.useRef();
7327
+ const groupRef = hooks.useRef(null);
7328
+ const [open, setOpen] = useLayoutState(['groups', id, 'open'], false);
7329
+ const [sticky, setSticky] = hooks.useState(false);
7330
+ const onShow = hooks.useCallback(() => setOpen(true), [setOpen]);
7331
+ const [ordering, setOrdering] = hooks.useState([]);
7332
+ const [newItemAdded, setNewItemAdded] = hooks.useState(false);
6777
7333
 
6778
- // To be consistent with the FEEL editor, set focus at start of input
6779
- // this ensures clean editing experience when switching with the keyboard
6780
- ref.current = {
6781
- focus: () => {
6782
- const input = inputRef.current;
6783
- if (!input) {
6784
- return;
7334
+ // Flag to mark that add button was clicked in the last render cycle
7335
+ const [addTriggered, setAddTriggered] = hooks.useState(false);
7336
+ const prevItems = usePrevious(items);
7337
+ const prevElement = usePrevious(element);
7338
+ const elementChanged = element !== prevElement;
7339
+ const shouldHandleEffects = !elementChanged && (shouldSort || shouldOpen);
7340
+
7341
+ // reset initial ordering when element changes (before first render)
7342
+ if (elementChanged) {
7343
+ setOrdering(createOrdering(shouldSort ? sortItems(items) : items));
7344
+ }
7345
+
7346
+ // keep ordering in sync to items - and open changes
7347
+
7348
+ // (0) set initial ordering from given items
7349
+ hooks.useEffect(() => {
7350
+ if (!prevItems || !shouldSort) {
7351
+ setOrdering(createOrdering(items));
7352
+ }
7353
+ }, [items, element]);
7354
+
7355
+ // (1) items were added
7356
+ hooks.useEffect(() => {
7357
+ // reset addTriggered flag
7358
+ setAddTriggered(false);
7359
+ if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
7360
+ let add = [];
7361
+ items.forEach(item => {
7362
+ if (!ordering.includes(item.id)) {
7363
+ add.push(item.id);
7364
+ }
7365
+ });
7366
+ let newOrdering = ordering;
7367
+
7368
+ // open if not open, configured and triggered by add button
7369
+ //
7370
+ // TODO(marstamm): remove once we refactor layout handling for listGroups.
7371
+ // Ideally, opening should be handled as part of the `add` callback and
7372
+ // not be a concern for the ListGroup component.
7373
+ if (addTriggered && !open && shouldOpen) {
7374
+ toggleOpen();
6785
7375
  }
6786
- input.focus();
6787
- input.setSelectionRange(0, 0);
7376
+
7377
+ // filter when not open and configured
7378
+ if (!open && shouldSort) {
7379
+ newOrdering = createOrdering(sortItems(items));
7380
+ }
7381
+
7382
+ // add new items on top or bottom depending on sorting behavior
7383
+ newOrdering = newOrdering.filter(item => !add.includes(item));
7384
+ if (shouldSort) {
7385
+ newOrdering.unshift(...add);
7386
+ } else {
7387
+ newOrdering.push(...add);
7388
+ }
7389
+ setOrdering(newOrdering);
7390
+ setNewItemAdded(addTriggered);
7391
+ } else {
7392
+ setNewItemAdded(false);
7393
+ }
7394
+ }, [items, open, shouldHandleEffects, addTriggered]);
7395
+
7396
+ // (2) sort items on open if shouldSort is set
7397
+ hooks.useEffect(() => {
7398
+ if (shouldSort && open && !newItemAdded) {
7399
+ setOrdering(createOrdering(sortItems(items)));
7400
+ }
7401
+ }, [open, shouldSort]);
7402
+
7403
+ // (3) items were deleted
7404
+ hooks.useEffect(() => {
7405
+ if (shouldHandleEffects && prevItems && items.length < prevItems.length) {
7406
+ let keep = [];
7407
+ ordering.forEach(o => {
7408
+ if (getItem(items, o)) {
7409
+ keep.push(o);
7410
+ }
7411
+ });
7412
+ setOrdering(keep);
6788
7413
  }
7414
+ }, [items, shouldHandleEffects]);
7415
+
7416
+ // set css class when group is sticky to top
7417
+ useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);
7418
+ const toggleOpen = () => setOpen(!open);
7419
+ const hasItems = !!items.length;
7420
+ const propertiesPanelContext = {
7421
+ ...hooks.useContext(LayoutContext),
7422
+ onShow
6789
7423
  };
6790
- return jsxRuntime.jsx("textarea", {
6791
- id: prefixId$4(id),
6792
- type: "text",
6793
- ref: inputRef,
6794
- name: id,
6795
- spellCheck: "false",
6796
- autoComplete: "off",
6797
- disabled: disabled,
6798
- class: "bio-properties-panel-input",
6799
- onInput: e => onInput(e.target.value),
6800
- onFocus: onFocus,
6801
- onBlur: onBlur,
6802
- value: value || '',
6803
- "data-gramm": "false"
7424
+ const handleAddClick = e => {
7425
+ setAddTriggered(true);
7426
+ add(e);
7427
+ };
7428
+ const allErrors = useErrors();
7429
+ const hasError = items.some(item => {
7430
+ if (allErrors[item.id]) {
7431
+ return true;
7432
+ }
7433
+ if (!item.entries) {
7434
+ return;
7435
+ }
7436
+
7437
+ // also check if the error is nested, e.g. for name-value entries
7438
+ return item.entries.some(entry => allErrors[entry.id]);
6804
7439
  });
6805
- });
6806
- const OptionalFeelToggleSwitch = React.forwardRef((props, ref) => {
6807
- const {
6808
- id,
6809
- onInput,
6810
- value,
6811
- onFocus,
6812
- onBlur,
6813
- switcherLabel
6814
- } = props;
6815
- const inputRef = hooks.useRef();
7440
+ return jsxRuntime.jsxs("div", {
7441
+ class: "bio-properties-panel-group",
7442
+ "data-group-id": 'group-' + id,
7443
+ ref: groupRef,
7444
+ children: [jsxRuntime.jsxs("div", {
7445
+ class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
7446
+ onClick: hasItems ? toggleOpen : noop$1,
7447
+ children: [jsxRuntime.jsx("div", {
7448
+ title: label,
7449
+ class: "bio-properties-panel-group-header-title",
7450
+ children: jsxRuntime.jsx(TooltipWrapper, {
7451
+ value: props.tooltip,
7452
+ forId: 'group-' + id,
7453
+ element: element,
7454
+ parent: groupRef,
7455
+ children: label
7456
+ })
7457
+ }), jsxRuntime.jsxs("div", {
7458
+ class: "bio-properties-panel-group-header-buttons",
7459
+ children: [add ? jsxRuntime.jsxs("button", {
7460
+ title: "Create new list item",
7461
+ class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
7462
+ onClick: handleAddClick,
7463
+ children: [jsxRuntime.jsx(CreateIcon, {}), !hasItems ? jsxRuntime.jsx("span", {
7464
+ class: "bio-properties-panel-add-entry-label",
7465
+ children: "Create"
7466
+ }) : null]
7467
+ }) : null, hasItems ? jsxRuntime.jsx("div", {
7468
+ title: `List contains ${items.length} item${items.length != 1 ? 's' : ''}`,
7469
+ class: classnames('bio-properties-panel-list-badge', hasError ? 'bio-properties-panel-list-badge--error' : ''),
7470
+ children: items.length
7471
+ }) : null, hasItems ? jsxRuntime.jsx("button", {
7472
+ title: "Toggle section",
7473
+ class: "bio-properties-panel-group-header-button bio-properties-panel-arrow",
7474
+ children: jsxRuntime.jsx(ArrowIcon, {
7475
+ class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
7476
+ })
7477
+ }) : null]
7478
+ })]
7479
+ }), jsxRuntime.jsx("div", {
7480
+ class: classnames('bio-properties-panel-list', open && hasItems ? 'open' : ''),
7481
+ children: jsxRuntime.jsx(LayoutContext.Provider, {
7482
+ value: propertiesPanelContext,
7483
+ children: ordering.map((o, index) => {
7484
+ const item = getItem(items, o);
7485
+ if (!item) {
7486
+ return;
7487
+ }
7488
+ const {
7489
+ id
7490
+ } = item;
6816
7491
 
6817
- // To be consistent with the FEEL editor, set focus at start of input
6818
- // this ensures clean editing experience when switching with the keyboard
6819
- ref.current = {
6820
- focus: () => {
6821
- const input = inputRef.current;
6822
- if (!input) {
6823
- return;
6824
- }
6825
- input.focus();
6826
- }
6827
- };
6828
- return jsxRuntime.jsx(ToggleSwitch, {
6829
- id: id,
6830
- value: value,
6831
- inputRef: inputRef,
6832
- onInput: onInput,
6833
- onFocus: onFocus,
6834
- onBlur: onBlur,
6835
- switcherLabel: switcherLabel
7492
+ // if item was added, open it
7493
+ // Existing items will not be affected as autoOpen is only applied on first render
7494
+ const autoOpen = newItemAdded;
7495
+ return preact.createElement(ListItem, {
7496
+ ...item,
7497
+ autoOpen: autoOpen,
7498
+ element: element,
7499
+ index: index,
7500
+ key: id
7501
+ });
7502
+ })
7503
+ })
7504
+ })]
6836
7505
  });
6837
- });
6838
- React.forwardRef((props, ref) => {
6839
- const {
6840
- id,
6841
- disabled,
6842
- onInput,
6843
- value,
6844
- onFocus,
6845
- onBlur
6846
- } = props;
6847
- const inputRef = hooks.useRef();
6848
- const handleChange = ({
6849
- target
6850
- }) => {
6851
- onInput(target.checked);
6852
- };
7506
+ }
6853
7507
 
6854
- // To be consistent with the FEEL editor, set focus at start of input
6855
- // this ensures clean editing experience when switching with the keyboard
6856
- ref.current = {
6857
- focus: () => {
6858
- const input = inputRef.current;
6859
- if (!input) {
6860
- return;
6861
- }
6862
- input.focus();
6863
- }
6864
- };
6865
- return jsxRuntime.jsx("input", {
6866
- ref: inputRef,
6867
- id: prefixId$4(id),
6868
- name: id,
6869
- onFocus: onFocus,
6870
- onBlur: onBlur,
6871
- type: "checkbox",
6872
- class: "bio-properties-panel-input",
6873
- onChange: handleChange,
6874
- checked: value,
6875
- disabled: disabled
6876
- });
6877
- });
7508
+ // helpers ////////////////////
6878
7509
 
6879
7510
  /**
6880
- * @param {Object} props
6881
- * @param {Object} props.element
6882
- * @param {String} props.id
6883
- * @param {String} props.description
6884
- * @param {Boolean} props.debounce
6885
- * @param {Boolean} props.disabled
6886
- * @param {Boolean} props.feel
6887
- * @param {String} props.label
6888
- * @param {Function} props.getValue
6889
- * @param {Function} props.setValue
6890
- * @param {Function} props.tooltipContainer
6891
- * @param {Function} props.validate
6892
- * @param {Function} props.show
6893
- * @param {Function} props.example
6894
- * @param {Function} props.variables
6895
- * @param {Function} props.onFocus
6896
- * @param {Function} props.onBlur
6897
- * @param {string|import('preact').Component} props.tooltip
7511
+ * Sorts given items alphanumeric by label
6898
7512
  */
6899
- function FeelEntry(props) {
7513
+ function sortItems(items) {
7514
+ return minDash.sortBy(items, i => i.label.toLowerCase());
7515
+ }
7516
+ function getItem(items, id) {
7517
+ return minDash.find(items, i => i.id === id);
7518
+ }
7519
+ function createOrdering(items) {
7520
+ return items.map(i => i.id);
7521
+ }
7522
+ function Checkbox(props) {
6900
7523
  const {
6901
- element,
6902
7524
  id,
6903
- description,
6904
- debounce,
6905
- disabled,
6906
- feel,
6907
7525
  label,
6908
- getValue,
6909
- setValue,
6910
- tooltipContainer,
6911
- hostLanguage,
6912
- singleLine,
6913
- validate,
6914
- show = noop$1,
6915
- example,
6916
- variables,
7526
+ onChange,
7527
+ disabled,
7528
+ value = false,
6917
7529
  onFocus,
6918
7530
  onBlur,
6919
7531
  tooltip
6920
7532
  } = props;
6921
- const [validationError, setValidationError] = hooks.useState(null);
6922
- const [localError, setLocalError] = hooks.useState(null);
6923
- let value = getValue(element);
7533
+ const [localValue, setLocalValue] = hooks.useState(value);
7534
+ const handleChangeCallback = ({
7535
+ target
7536
+ }) => {
7537
+ onChange(target.checked);
7538
+ };
7539
+ const handleChange = e => {
7540
+ handleChangeCallback(e);
7541
+ setLocalValue(e.target.value);
7542
+ };
6924
7543
  hooks.useEffect(() => {
6925
- if (minDash.isFunction(validate)) {
6926
- const newValidationError = validate(value) || null;
6927
- setValidationError(newValidationError);
7544
+ if (value === localValue) {
7545
+ return;
6928
7546
  }
7547
+ setLocalValue(value);
6929
7548
  }, [value]);
6930
- const onInput = useStaticCallback(newValue => {
6931
- let newValidationError = null;
6932
- if (minDash.isFunction(validate)) {
6933
- newValidationError = validate(newValue) || null;
6934
- }
6935
-
6936
- // don't create multiple commandStack entries for the same value
6937
- if (newValue !== value) {
6938
- setValue(newValue, newValidationError);
6939
- }
6940
- setValidationError(newValidationError);
6941
- });
6942
- const onError = hooks.useCallback(err => {
6943
- setLocalError(err);
6944
- }, []);
6945
- const temporaryError = useError(id);
6946
- const error = localError || temporaryError || validationError;
7549
+ const ref = useShowEntryEvent(id);
6947
7550
  return jsxRuntime.jsxs("div", {
6948
- class: classnames(props.class, 'bio-properties-panel-entry', error ? 'has-error' : ''),
6949
- "data-entry-id": id,
6950
- children: [preact.createElement(FeelTextfield, {
6951
- ...props,
6952
- debounce: debounce,
6953
- disabled: disabled,
6954
- feel: feel,
6955
- id: id,
6956
- key: element,
6957
- label: label,
6958
- onInput: onInput,
6959
- onError: onError,
7551
+ class: "bio-properties-panel-checkbox",
7552
+ children: [jsxRuntime.jsx("input", {
7553
+ ref: ref,
7554
+ id: prefixId$4(id),
7555
+ name: id,
6960
7556
  onFocus: onFocus,
6961
7557
  onBlur: onBlur,
6962
- example: example,
6963
- hostLanguage: hostLanguage,
6964
- singleLine: singleLine,
6965
- show: show,
6966
- value: value,
6967
- variables: variables,
6968
- tooltipContainer: tooltipContainer,
6969
- OptionalComponent: props.OptionalComponent,
6970
- tooltip: tooltip
6971
- }), error && jsxRuntime.jsx("div", {
6972
- class: "bio-properties-panel-error",
6973
- children: error
6974
- }), jsxRuntime.jsx(Description$1, {
6975
- forId: id,
6976
- element: element,
6977
- value: description
6978
- })]
6979
- });
6980
- }
6981
-
6982
- /**
6983
- * @param {Object} props
6984
- * @param {Object} props.element
6985
- * @param {String} props.id
6986
- * @param {String} props.description
6987
- * @param {Boolean} props.debounce
6988
- * @param {Boolean} props.disabled
6989
- * @param {String} props.max
6990
- * @param {String} props.min
6991
- * @param {String} props.step
6992
- * @param {Boolean} props.feel
6993
- * @param {String} props.label
6994
- * @param {Function} props.getValue
6995
- * @param {Function} props.setValue
6996
- * @param {Function} props.tooltipContainer
6997
- * @param {Function} props.validate
6998
- * @param {Function} props.show
6999
- * @param {Function} props.example
7000
- * @param {Function} props.variables
7001
- * @param {Function} props.onFocus
7002
- * @param {Function} props.onBlur
7003
- */
7004
- function FeelNumberEntry(props) {
7005
- return jsxRuntime.jsx(FeelEntry, {
7006
- class: "bio-properties-panel-feel-number",
7007
- OptionalComponent: OptionalFeelNumberField,
7008
- ...props
7009
- });
7010
- }
7011
-
7012
- /**
7013
- * @param {Object} props
7014
- * @param {Object} props.element
7015
- * @param {String} props.id
7016
- * @param {String} props.description
7017
- * @param {Boolean} props.debounce
7018
- * @param {Boolean} props.disabled
7019
- * @param {Boolean} props.feel
7020
- * @param {String} props.label
7021
- * @param {Function} props.getValue
7022
- * @param {Function} props.setValue
7023
- * @param {Function} props.tooltipContainer
7024
- * @param {Function} props.validate
7025
- * @param {Function} props.show
7026
- * @param {Function} props.example
7027
- * @param {Function} props.variables
7028
- * @param {Function} props.onFocus
7029
- * @param {Function} props.onBlur
7030
- */
7031
- function FeelToggleSwitchEntry(props) {
7032
- return jsxRuntime.jsx(FeelEntry, {
7033
- class: "bio-properties-panel-feel-toggle-switch",
7034
- OptionalComponent: OptionalFeelToggleSwitch,
7035
- ...props
7558
+ type: "checkbox",
7559
+ class: "bio-properties-panel-input",
7560
+ onChange: handleChange,
7561
+ checked: localValue,
7562
+ disabled: disabled
7563
+ }), jsxRuntime.jsx("label", {
7564
+ for: prefixId$4(id),
7565
+ class: "bio-properties-panel-label",
7566
+ children: jsxRuntime.jsx(TooltipWrapper, {
7567
+ value: tooltip,
7568
+ forId: id,
7569
+ element: props.element,
7570
+ children: label
7571
+ })
7572
+ })]
7036
7573
  });
7037
7574
  }
7038
7575
 
@@ -7041,37 +7578,54 @@ function FeelToggleSwitchEntry(props) {
7041
7578
  * @param {Object} props.element
7042
7579
  * @param {String} props.id
7043
7580
  * @param {String} props.description
7044
- * @param {String} props.hostLanguage
7045
- * @param {Boolean} props.singleLine
7046
- * @param {Boolean} props.debounce
7047
- * @param {Boolean} props.disabled
7048
- * @param {Boolean} props.feel
7049
7581
  * @param {String} props.label
7050
7582
  * @param {Function} props.getValue
7051
7583
  * @param {Function} props.setValue
7052
- * @param {Function} props.tooltipContainer
7053
- * @param {Function} props.validate
7054
- * @param {Function} props.show
7055
- * @param {Function} props.example
7056
- * @param {Function} props.variables
7057
7584
  * @param {Function} props.onFocus
7058
7585
  * @param {Function} props.onBlur
7586
+ * @param {string|import('preact').Component} props.tooltip
7587
+ * @param {boolean} [props.disabled]
7059
7588
  */
7060
- function FeelTemplatingEntry(props) {
7061
- return jsxRuntime.jsx(FeelEntry, {
7062
- class: "bio-properties-panel-feel-templating",
7063
- OptionalComponent: CodeEditor$1,
7064
- ...props
7589
+ function CheckboxEntry(props) {
7590
+ const {
7591
+ element,
7592
+ id,
7593
+ description,
7594
+ label,
7595
+ getValue,
7596
+ setValue,
7597
+ disabled,
7598
+ onFocus,
7599
+ onBlur,
7600
+ tooltip
7601
+ } = props;
7602
+ const value = getValue(element);
7603
+ const error = useError(id);
7604
+ return jsxRuntime.jsxs("div", {
7605
+ class: "bio-properties-panel-entry bio-properties-panel-checkbox-entry",
7606
+ "data-entry-id": id,
7607
+ children: [jsxRuntime.jsx(Checkbox, {
7608
+ disabled: disabled,
7609
+ id: id,
7610
+ label: label,
7611
+ onChange: setValue,
7612
+ onFocus: onFocus,
7613
+ onBlur: onBlur,
7614
+ value: value,
7615
+ tooltip: tooltip,
7616
+ element: element
7617
+ }, element), error && jsxRuntime.jsx("div", {
7618
+ class: "bio-properties-panel-error",
7619
+ children: error
7620
+ }), jsxRuntime.jsx(Description$1, {
7621
+ forId: id,
7622
+ element: element,
7623
+ value: description
7624
+ })]
7065
7625
  });
7066
7626
  }
7067
7627
  function isEdited$5(node) {
7068
- if (!node) {
7069
- return false;
7070
- }
7071
- if (node.type === 'checkbox') {
7072
- return !!node.checked || node.classList.contains('edited');
7073
- }
7074
- return !!node.value || node.classList.contains('edited');
7628
+ return node && !!node.checked;
7075
7629
  }
7076
7630
 
7077
7631
  // helpers /////////////////
@@ -7567,6 +8121,9 @@ function textToLabel(text) {
7567
8121
  }
7568
8122
  return null;
7569
8123
  }
8124
+ function isValidDotPath(path) {
8125
+ return /^\w+(\.\w+)*$/.test(path);
8126
+ }
7570
8127
  const INPUTS = ['checkbox', 'checklist', 'datetime', 'number', 'radio', 'select', 'taglist', 'textfield', 'textarea'];
7571
8128
  const VALUES_INPUTS = ['checklist', 'radio', 'select', 'taglist'];
7572
8129
 
@@ -7577,6 +8134,7 @@ const labelsByType = {
7577
8134
  columns: 'COLUMNS',
7578
8135
  default: 'FORM',
7579
8136
  datetime: 'DATETIME',
8137
+ group: 'GROUP',
7580
8138
  image: 'IMAGE VIEW',
7581
8139
  number: 'NUMBER',
7582
8140
  radio: 'RADIO',
@@ -7592,6 +8150,9 @@ const PropertiesPanelHeaderProvider = {
7592
8150
  const {
7593
8151
  type
7594
8152
  } = field;
8153
+ if (type === 'spacer') {
8154
+ return '';
8155
+ }
7595
8156
  if (type === 'text') {
7596
8157
  return textToLabel(field.text);
7597
8158
  }
@@ -7723,7 +8284,7 @@ function AltTextEntry(props) {
7723
8284
  component: AltText,
7724
8285
  editField: editField,
7725
8286
  field: field,
7726
- isEdited: isEdited$5
8287
+ isEdited: isEdited$6
7727
8288
  });
7728
8289
  }
7729
8290
  return entries;
@@ -7846,7 +8407,7 @@ function DescriptionEntry(props) {
7846
8407
  component: Description,
7847
8408
  editField: editField,
7848
8409
  field: field,
7849
- isEdited: isEdited$5
8410
+ isEdited: isEdited$6
7850
8411
  });
7851
8412
  }
7852
8413
  return entries;
@@ -8133,7 +8694,7 @@ function DisabledEntry(props) {
8133
8694
  component: Disabled,
8134
8695
  editField: editField,
8135
8696
  field: field,
8136
- isEdited: isEdited$7
8697
+ isEdited: isEdited$8
8137
8698
  });
8138
8699
  }
8139
8700
  return entries;
@@ -8268,7 +8829,7 @@ function Key$1(props) {
8268
8829
  field,
8269
8830
  id
8270
8831
  } = props;
8271
- const formFieldRegistry = useService('formFieldRegistry');
8832
+ const pathRegistry = useService('pathRegistry');
8272
8833
  const debounce = useService('debounce');
8273
8834
  const path = ['key'];
8274
8835
  const getValue = () => {
@@ -8281,17 +8842,32 @@ function Key$1(props) {
8281
8842
  return editField(field, path, value);
8282
8843
  };
8283
8844
  const validate = value => {
8845
+ if (value === field.key) {
8846
+ return null;
8847
+ }
8284
8848
  if (minDash.isUndefined(value) || !value.length) {
8285
8849
  return 'Must not be empty.';
8286
8850
  }
8287
- if (/\s/.test(value)) {
8288
- return 'Must not contain spaces.';
8851
+ if (value && !isValidDotPath(value)) {
8852
+ return 'Must be a variable or a dot separated path.';
8289
8853
  }
8290
- const assigned = formFieldRegistry._keys.assigned(value);
8291
- if (assigned && assigned !== field) {
8292
- return 'Must be unique.';
8854
+ const hasIntegerPathSegment = value.split('.').some(segment => /^\d+$/.test(segment));
8855
+ if (hasIntegerPathSegment) {
8856
+ return 'Must not contain numerical path segments.';
8293
8857
  }
8294
- return null;
8858
+ const replacements = {
8859
+ [field.id]: value.split('.')
8860
+ };
8861
+ const oldPath = pathRegistry.getValuePath(field);
8862
+ const newPath = pathRegistry.getValuePath(field, {
8863
+ replacements
8864
+ });
8865
+
8866
+ // unclaim temporarily to avoid self-conflicts
8867
+ pathRegistry.unclaimPath(oldPath);
8868
+ const canClaim = pathRegistry.canClaimPath(newPath, true);
8869
+ pathRegistry.claimPath(oldPath, true);
8870
+ return canClaim ? null : 'Must not conflict with other key/path assignments.';
8295
8871
  };
8296
8872
  return TextfieldEntry({
8297
8873
  debounce,
@@ -8306,6 +8882,147 @@ function Key$1(props) {
8306
8882
  });
8307
8883
  }
8308
8884
 
8885
+ function PathEntry(props) {
8886
+ const {
8887
+ editField,
8888
+ field
8889
+ } = props;
8890
+ const {
8891
+ type
8892
+ } = field;
8893
+ const entries = [];
8894
+ if (type === 'group') {
8895
+ entries.push({
8896
+ id: 'path',
8897
+ component: Path,
8898
+ editField: editField,
8899
+ field: field,
8900
+ isEdited: isEdited
8901
+ });
8902
+ }
8903
+ return entries;
8904
+ }
8905
+ function Path(props) {
8906
+ const {
8907
+ editField,
8908
+ field,
8909
+ id
8910
+ } = props;
8911
+ const debounce = useService('debounce');
8912
+ const pathRegistry = useService('pathRegistry');
8913
+ const path = ['path'];
8914
+ const getValue = () => {
8915
+ return minDash.get(field, path, '');
8916
+ };
8917
+ const setValue = (value, error) => {
8918
+ if (error) {
8919
+ return;
8920
+ }
8921
+ return editField(field, path, value);
8922
+ };
8923
+ const validate = value => {
8924
+ if (!value || value === field.path) {
8925
+ return null;
8926
+ }
8927
+ if (value && !isValidDotPath(value)) {
8928
+ return 'Must be empty, a variable or a dot separated path';
8929
+ }
8930
+ const hasIntegerPathSegment = value && value.split('.').some(segment => /^\d+$/.test(segment));
8931
+ if (hasIntegerPathSegment) {
8932
+ return 'Must not contain numerical path segments.';
8933
+ }
8934
+ const options = value && {
8935
+ replacements: {
8936
+ [field.id]: [value]
8937
+ }
8938
+ } || {};
8939
+ const canClaim = pathRegistry.executeRecursivelyOnFields(field, ({
8940
+ field,
8941
+ isClosed
8942
+ }) => {
8943
+ const path = pathRegistry.getValuePath(field, options);
8944
+ return pathRegistry.canClaimPath(path, isClosed);
8945
+ });
8946
+ if (!canClaim) {
8947
+ return 'Must not cause two binding paths to colide';
8948
+ }
8949
+ };
8950
+ return TextfieldEntry({
8951
+ debounce,
8952
+ description: 'Where the child variables of this component are pathed to.',
8953
+ element: field,
8954
+ getValue,
8955
+ id,
8956
+ label: 'Path',
8957
+ tooltip: 'Routes the children of this component into a form variable, may be left empty to route at the root level.',
8958
+ setValue,
8959
+ validate
8960
+ });
8961
+ }
8962
+
8963
+ function simpleBoolEntryFactory(options) {
8964
+ const {
8965
+ id,
8966
+ label,
8967
+ description,
8968
+ path,
8969
+ props
8970
+ } = options;
8971
+ const {
8972
+ editField,
8973
+ field
8974
+ } = props;
8975
+ return {
8976
+ id,
8977
+ label,
8978
+ path,
8979
+ field,
8980
+ editField,
8981
+ description,
8982
+ component: SimpleBoolComponent,
8983
+ isEdited: isEdited$5
8984
+ };
8985
+ }
8986
+ const SimpleBoolComponent = props => {
8987
+ const {
8988
+ id,
8989
+ label,
8990
+ path,
8991
+ field,
8992
+ editField,
8993
+ description
8994
+ } = props;
8995
+ const getValue = () => minDash.get(field, path, '');
8996
+ const setValue = value => editField(field, path, value || false);
8997
+ return CheckboxEntry({
8998
+ element: field,
8999
+ getValue,
9000
+ id,
9001
+ label,
9002
+ setValue,
9003
+ description
9004
+ });
9005
+ };
9006
+
9007
+ function GroupEntries(props) {
9008
+ const {
9009
+ field
9010
+ } = props;
9011
+ const {
9012
+ type
9013
+ } = field;
9014
+ if (type !== 'group') {
9015
+ return [];
9016
+ }
9017
+ const entries = [simpleBoolEntryFactory({
9018
+ id: 'showOutline',
9019
+ path: ['showOutline'],
9020
+ label: 'Show outline',
9021
+ props
9022
+ })];
9023
+ return entries;
9024
+ }
9025
+
8309
9026
  function LabelEntry(props) {
8310
9027
  const {
8311
9028
  field,
@@ -8323,7 +9040,7 @@ function LabelEntry(props) {
8323
9040
  component: DateLabel,
8324
9041
  editField,
8325
9042
  field,
8326
- isEdited: isEdited$5
9043
+ isEdited: isEdited$6
8327
9044
  });
8328
9045
  }
8329
9046
  if (subtype === formJsViewer.DATETIME_SUBTYPES.TIME || subtype === formJsViewer.DATETIME_SUBTYPES.DATETIME) {
@@ -8332,16 +9049,16 @@ function LabelEntry(props) {
8332
9049
  component: TimeLabel,
8333
9050
  editField,
8334
9051
  field,
8335
- isEdited: isEdited$5
9052
+ isEdited: isEdited$6
8336
9053
  });
8337
9054
  }
8338
- } else if (INPUTS.includes(type) || type === 'button') {
9055
+ } else if (INPUTS.includes(type) || type === 'button' || type === 'group') {
8339
9056
  entries.push({
8340
9057
  id: 'label',
8341
9058
  component: Label$1,
8342
9059
  editField,
8343
9060
  field,
8344
- isEdited: isEdited$5
9061
+ isEdited: isEdited$6
8345
9062
  });
8346
9063
  }
8347
9064
  return entries;
@@ -8363,12 +9080,13 @@ function Label$1(props) {
8363
9080
  const setValue = value => {
8364
9081
  return editField(field, path, value || '');
8365
9082
  };
9083
+ const label = field.type === 'group' ? 'Group label' : 'Field label';
8366
9084
  return FeelTemplatingEntry({
8367
9085
  debounce,
8368
9086
  element: field,
8369
9087
  getValue,
8370
9088
  id,
8371
- label: 'Field label',
9089
+ label,
8372
9090
  singleLine: true,
8373
9091
  setValue,
8374
9092
  variables
@@ -8446,7 +9164,7 @@ function SourceEntry(props) {
8446
9164
  component: Source,
8447
9165
  editField: editField,
8448
9166
  field: field,
8449
- isEdited: isEdited$5
9167
+ isEdited: isEdited$6
8450
9168
  });
8451
9169
  }
8452
9170
  return entries;
@@ -8503,7 +9221,7 @@ function TextEntry(props) {
8503
9221
  component: Text,
8504
9222
  editField: editField,
8505
9223
  field: field,
8506
- isEdited: isEdited$5
9224
+ isEdited: isEdited$6
8507
9225
  }];
8508
9226
 
8509
9227
  // todo: skipped to make the release without too much risk
@@ -8572,7 +9290,7 @@ function SpacerEntry(props) {
8572
9290
  entries.push({
8573
9291
  id: id + '-height',
8574
9292
  component: SpacerHeight,
8575
- isEdited: isEdited$6,
9293
+ isEdited: isEdited$7,
8576
9294
  editField,
8577
9295
  field
8578
9296
  });
@@ -8623,7 +9341,7 @@ function NumberEntries(props) {
8623
9341
  entries.push({
8624
9342
  id: id + '-decimalDigits',
8625
9343
  component: NumberDecimalDigits,
8626
- isEdited: isEdited$6,
9344
+ isEdited: isEdited$7,
8627
9345
  editField,
8628
9346
  field
8629
9347
  });
@@ -8727,7 +9445,7 @@ function NumberSerializationEntry(props) {
8727
9445
  entries.push({
8728
9446
  id: 'serialize-to-string',
8729
9447
  component: SerializeToString,
8730
- isEdited: isEdited$8,
9448
+ isEdited: isEdited$5,
8731
9449
  editField,
8732
9450
  field
8733
9451
  });
@@ -8786,7 +9504,7 @@ function DateTimeEntry(props) {
8786
9504
  entries.push({
8787
9505
  id: 'use24h',
8788
9506
  component: Use24h,
8789
- isEdited: isEdited$8,
9507
+ isEdited: isEdited$5,
8790
9508
  editField,
8791
9509
  field
8792
9510
  });
@@ -8899,7 +9617,7 @@ function DateTimeConstraintsEntry(props) {
8899
9617
  entries.push({
8900
9618
  id: id + '-disallowPassedDates',
8901
9619
  component: DisallowPassedDates,
8902
- isEdited: isEdited$8,
9620
+ isEdited: isEdited$5,
8903
9621
  editField,
8904
9622
  field
8905
9623
  });
@@ -9000,50 +9718,6 @@ function TimeFormatSelect(props) {
9000
9718
  });
9001
9719
  }
9002
9720
 
9003
- function simpleBoolEntryFactory(options) {
9004
- const {
9005
- id,
9006
- label,
9007
- description,
9008
- path,
9009
- props
9010
- } = options;
9011
- const {
9012
- editField,
9013
- field
9014
- } = props;
9015
- return {
9016
- id,
9017
- label,
9018
- path,
9019
- field,
9020
- editField,
9021
- description,
9022
- component: SimpleBoolComponent,
9023
- isEdited: isEdited$8
9024
- };
9025
- }
9026
- const SimpleBoolComponent = props => {
9027
- const {
9028
- id,
9029
- label,
9030
- path,
9031
- field,
9032
- editField,
9033
- description
9034
- } = props;
9035
- const getValue = () => minDash.get(field, path, '');
9036
- const setValue = value => editField(field, path, value);
9037
- return CheckboxEntry({
9038
- element: field,
9039
- getValue,
9040
- id,
9041
- label,
9042
- setValue,
9043
- description
9044
- });
9045
- };
9046
-
9047
9721
  function SelectEntries(props) {
9048
9722
  const {
9049
9723
  field
@@ -9525,7 +10199,7 @@ function AdornerEntry(props) {
9525
10199
  entries.push({
9526
10200
  id: 'prefix-adorner',
9527
10201
  component: PrefixAdorner,
9528
- isEdited: isEdited$5,
10202
+ isEdited: isEdited$6,
9529
10203
  editField,
9530
10204
  field,
9531
10205
  onChange,
@@ -9534,7 +10208,7 @@ function AdornerEntry(props) {
9534
10208
  entries.push({
9535
10209
  id: 'suffix-adorner',
9536
10210
  component: SuffixAdorner,
9537
- isEdited: isEdited$5,
10211
+ isEdited: isEdited$6,
9538
10212
  editField,
9539
10213
  field,
9540
10214
  onChange,
@@ -9604,7 +10278,7 @@ function ReadonlyEntry(props) {
9604
10278
  component: Readonly,
9605
10279
  editField: editField,
9606
10280
  field: field,
9607
- isEdited: isEdited$5
10281
+ isEdited: isEdited$6
9608
10282
  });
9609
10283
  }
9610
10284
  return entries;
@@ -9649,7 +10323,7 @@ function ConditionEntry(props) {
9649
10323
  component: Condition,
9650
10324
  editField: editField,
9651
10325
  field: field,
9652
- isEdited: isEdited$5
10326
+ isEdited: isEdited$6
9653
10327
  }];
9654
10328
  }
9655
10329
  function Condition(props) {
@@ -9697,7 +10371,7 @@ function ValuesExpressionEntry(props) {
9697
10371
  id: id + '-expression',
9698
10372
  component: ValuesExpression,
9699
10373
  label: 'Values expression',
9700
- isEdited: isEdited$5,
10374
+ isEdited: isEdited$6,
9701
10375
  editField,
9702
10376
  field
9703
10377
  }];
@@ -9749,6 +10423,12 @@ function GeneralGroup(field, editField, getService) {
9749
10423
  }), ...KeyEntry({
9750
10424
  field,
9751
10425
  editField
10426
+ }), ...PathEntry({
10427
+ field,
10428
+ editField
10429
+ }), ...GroupEntries({
10430
+ field,
10431
+ editField
9752
10432
  }), ...DefaultOptionEntry({
9753
10433
  field,
9754
10434
  editField
@@ -9863,7 +10543,7 @@ function ValidationGroup(field, editField) {
9863
10543
  component: Required,
9864
10544
  getValue,
9865
10545
  field,
9866
- isEdited: isEdited$8,
10546
+ isEdited: isEdited$5,
9867
10547
  onChange
9868
10548
  }];
9869
10549
  if (type === 'textfield') {
@@ -9883,14 +10563,14 @@ function ValidationGroup(field, editField) {
9883
10563
  component: MinLength,
9884
10564
  getValue,
9885
10565
  field,
9886
- isEdited: isEdited$5,
10566
+ isEdited: isEdited$6,
9887
10567
  onChange
9888
10568
  }, {
9889
10569
  id: 'maxLength',
9890
10570
  component: MaxLength,
9891
10571
  getValue,
9892
10572
  field,
9893
- isEdited: isEdited$5,
10573
+ isEdited: isEdited$6,
9894
10574
  onChange
9895
10575
  });
9896
10576
  }
@@ -9910,14 +10590,14 @@ function ValidationGroup(field, editField) {
9910
10590
  component: Min,
9911
10591
  getValue,
9912
10592
  field,
9913
- isEdited: isEdited$5,
10593
+ isEdited: isEdited$6,
9914
10594
  onChange
9915
10595
  }, {
9916
10596
  id: 'max',
9917
10597
  component: Max,
9918
10598
  getValue,
9919
10599
  field,
9920
- isEdited: isEdited$5,
10600
+ isEdited: isEdited$6,
9921
10601
  onChange
9922
10602
  });
9923
10603
  }