@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.es.js CHANGED
@@ -1,7 +1,7 @@
1
- import { FormFieldRegistry as FormFieldRegistry$1, clone, iconsByType, Text as Text$1, FormFields, formFields, FormContext, FormRenderContext, FormComponent, FormLayouter, getSchemaVariables, DATETIME_SUBTYPES, DATE_LABEL_PATH, TIME_LABEL_PATH, DATETIME_SUBTYPE_PATH, DATETIME_SUBTYPES_LABELS, TIME_SERIALISING_FORMAT_PATH, TIME_SERIALISING_FORMATS, TIME_INTERVAL_PATH, TIME_USE24H_PATH, DATE_DISALLOW_PAST_PATH, TIME_SERIALISINGFORMAT_LABELS, getValuesSource, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_PATHS, VALUES_SOURCES_LABELS, FeelExpressionLanguage, createFormContainer, createInjector, MarkdownModule, schemaVersion } from '@bpmn-io/form-js-viewer';
1
+ import { FormFieldRegistry as FormFieldRegistry$1, iconsByType, Text as Text$1, FormFields, formFields, FormContext, FormRenderContext, FormComponent, Importer, PathRegistry, FormLayouter, FieldFactory, runRecursively, clone, getSchemaVariables, DATETIME_SUBTYPES, DATE_LABEL_PATH, TIME_LABEL_PATH, DATETIME_SUBTYPE_PATH, DATETIME_SUBTYPES_LABELS, TIME_SERIALISING_FORMAT_PATH, TIME_SERIALISING_FORMATS, TIME_INTERVAL_PATH, TIME_USE24H_PATH, DATE_DISALLOW_PAST_PATH, TIME_SERIALISINGFORMAT_LABELS, getValuesSource, VALUES_SOURCES, VALUES_SOURCES_DEFAULTS, VALUES_SOURCES_PATHS, VALUES_SOURCES_LABELS, FeelExpressionLanguage, createFormContainer, createInjector, MarkdownModule, schemaVersion } from '@bpmn-io/form-js-viewer';
2
2
  export { schemaVersion } from '@bpmn-io/form-js-viewer';
3
3
  import Ids from 'ids';
4
- import { isArray, isFunction, isNumber, bind, assign, debounce, forEach, get, isObject, uniqueBy, sortBy, find, set as set$1, isString, isUndefined, without, has } from 'min-dash';
4
+ import { isArray, isFunction, isNumber, bind, assign, debounce, forEach, get, isObject, uniqueBy, sortBy, find, throttle as throttle$1, set as set$1, isString, isUndefined, without, has } from 'min-dash';
5
5
  import classnames from 'classnames';
6
6
  import { jsx, jsxs, Fragment as Fragment$1 } from 'preact/jsx-runtime';
7
7
  import { useContext, useState, useMemo, useEffect, useCallback, useRef as useRef$1, useLayoutEffect } from 'preact/hooks';
@@ -13,6 +13,8 @@ import { classes, query, closest, event, matches, domify } from 'min-dom';
13
13
  import { mutate } from 'array-move';
14
14
  import { FeelersEditor } from 'feelers';
15
15
  import FeelEditor from '@bpmn-io/feel-editor';
16
+ import { lineNumbers } from '@codemirror/view';
17
+ import * as focusTrap from 'focus-trap';
16
18
  import Big from 'big.js';
17
19
 
18
20
  var FN_REF = '__fn';
@@ -530,73 +532,6 @@ function DebounceFactory(config = true) {
530
532
  }
531
533
  DebounceFactory.$inject = ['config.debounce'];
532
534
 
533
- class FieldFactory {
534
- /**
535
- * @constructor
536
- *
537
- * @param { import('./FormFieldRegistry').default } formFieldRegistry
538
- * @param { import('@bpmn-io/form-js-viewer').FormFields } formFields
539
- */
540
- constructor(formFieldRegistry, formFields) {
541
- this._formFieldRegistry = formFieldRegistry;
542
- this._formFields = formFields;
543
- }
544
- create(attrs, applyDefaults = true) {
545
- const {
546
- id,
547
- key,
548
- type
549
- } = attrs;
550
- const fieldDefinition = this._formFields.get(type);
551
- if (!fieldDefinition) {
552
- throw new Error(`form field of type <${type}> not supported`);
553
- }
554
- const {
555
- config
556
- } = fieldDefinition;
557
- if (id && this._formFieldRegistry._ids.assigned(id)) {
558
- throw new Error(`ID <${id}> already assigned`);
559
- }
560
- if (key && this._formFieldRegistry._keys.assigned(key)) {
561
- throw new Error(`key <${key}> already assigned`);
562
- }
563
- const labelAttrs = applyDefaults && config.label ? {
564
- label: config.label
565
- } : {};
566
- const field = config.create({
567
- ...labelAttrs,
568
- ...attrs
569
- });
570
- this._ensureId(field);
571
- if (config.keyed) {
572
- this._ensureKey(field, applyDefaults);
573
- }
574
- return field;
575
- }
576
- _ensureId(field) {
577
- if (field.id) {
578
- this._formFieldRegistry._ids.claim(field.id, field);
579
- return;
580
- }
581
- let prefix = 'Field';
582
- if (field.type === 'default') {
583
- prefix = 'Form';
584
- }
585
- field.id = this._formFieldRegistry._ids.nextPrefixed(`${prefix}_`, field);
586
- }
587
- _ensureKey(field, applyDefaults) {
588
- if (field.key) {
589
- this._formFieldRegistry._keys.claim(field.key, field);
590
- return;
591
- }
592
- if (applyDefaults) {
593
- let prefix = 'field';
594
- field.key = this._formFieldRegistry._keys.nextPrefixed(`${prefix}_`, field);
595
- }
596
- }
597
- }
598
- FieldFactory.$inject = ['formFieldRegistry', 'formFields'];
599
-
600
535
  class FormFieldRegistry extends FormFieldRegistry$1 {
601
536
  /**
602
537
  * Updates a form fields id.
@@ -709,111 +644,7 @@ function calculateMaxColumnsWithAuto(autoCols) {
709
644
  return MAX_COLUMNS_PER_ROW - autoCols * 2;
710
645
  }
711
646
 
712
- class Importer {
713
- /**
714
- * @constructor
715
- * @param { import('../core/FormFieldRegistry').default } formFieldRegistry
716
- * @param { import('../core/FieldFactory').default } fieldFactory
717
- * @param { import('../core/FormLayouter').default } formLayouter
718
- */
719
- constructor(formFieldRegistry, fieldFactory, formLayouter) {
720
- this._formFieldRegistry = formFieldRegistry;
721
- this._fieldFactory = fieldFactory;
722
- this._formLayouter = formLayouter;
723
- }
724
-
725
- /**
726
- * Import schema creating rows, fields, attaching additional
727
- * information to each field and adding fields to the
728
- * field registry.
729
- *
730
- * Additional information attached:
731
- *
732
- * * `id` (unless present)
733
- * * `_parent`
734
- * * `_path`
735
- *
736
- * @param {any} schema
737
- *
738
- * @typedef {{ warnings: Error[], schema: any }} ImportResult
739
- * @returns {ImportResult}
740
- */
741
- importSchema(schema) {
742
- // TODO: Add warnings
743
- const warnings = [];
744
- try {
745
- const importedSchema = this.importFormField(clone(schema));
746
- this._formLayouter.calculateLayout(clone(importedSchema));
747
- return {
748
- schema: importedSchema,
749
- warnings
750
- };
751
- } catch (err) {
752
- err.warnings = warnings;
753
- throw err;
754
- }
755
- }
756
-
757
- /**
758
- * @param {{[x: string]: any}} fieldAttrs
759
- * @param {String} [parentId]
760
- * @param {number} [index]
761
- *
762
- * @return {any} field
763
- */
764
- importFormField(fieldAttrs, parentId, index) {
765
- const {
766
- components,
767
- id,
768
- key
769
- } = fieldAttrs;
770
- let parent, path;
771
- if (parentId) {
772
- parent = this._formFieldRegistry.get(parentId);
773
- }
774
-
775
- // validate <id> uniqueness
776
- if (id && this._formFieldRegistry._ids.assigned(id)) {
777
- throw new Error(`form field with id <${id}> already exists`);
778
- }
779
-
780
- // validate <key> uniqueness
781
- if (key && this._formFieldRegistry._keys.assigned(key)) {
782
- throw new Error(`form field with key <${key}> already exists`);
783
- }
784
-
785
- // set form field path
786
- path = parent ? [...parent._path, 'components', index] : [];
787
- const field = this._fieldFactory.create({
788
- ...fieldAttrs,
789
- _path: path,
790
- _parent: parent && parent.id
791
- }, false);
792
- this._formFieldRegistry.add(field);
793
- if (components) {
794
- field.components = this.importFormFields(components, field.id);
795
- }
796
- return field;
797
- }
798
-
799
- /**
800
- * @param {Array<any>} components
801
- * @param {string} parentId
802
- *
803
- * @return {Array<any>} imported components
804
- */
805
- importFormFields(components, parentId) {
806
- return components.map((component, index) => {
807
- return this.importFormField(component, parentId, index);
808
- });
809
- }
810
- }
811
- Importer.$inject = ['formFieldRegistry', 'fieldFactory', 'formLayouter'];
812
-
813
- var importModule = {
814
- importer: ['type', Importer]
815
- };
816
-
647
+ const emptyImage = createEmptyImage();
817
648
  function editorFormFieldClasses(type, {
818
649
  disabled = false
819
650
  } = {}) {
@@ -838,11 +669,10 @@ function editorFormFieldClasses(type, {
838
669
  * domElement.addEventListener('dragstart', dragger(dragMove));
839
670
  *
840
671
  * @param {Function} fn
841
- * @param {Element} dragPreview
842
672
  *
843
673
  * @return {Function} drag start callback function
844
674
  */
845
- function createDragger(fn, dragPreview) {
675
+ function createDragger$1(fn) {
846
676
  let self;
847
677
  let startX, startY;
848
678
 
@@ -852,9 +682,9 @@ function createDragger(fn, dragPreview) {
852
682
  startX = event.clientX;
853
683
  startY = event.clientY;
854
684
 
855
- // (1) prevent preview image
685
+ // (1) hide drag preview image
856
686
  if (event.dataTransfer) {
857
- event.dataTransfer.setDragImage(dragPreview, 0, 0);
687
+ event.dataTransfer.setDragImage(emptyImage, 0, 0);
858
688
  }
859
689
 
860
690
  // (2) setup drag listeners
@@ -862,7 +692,7 @@ function createDragger(fn, dragPreview) {
862
692
  // attach drag + cleanup event
863
693
  document.addEventListener('dragover', onDrag);
864
694
  document.addEventListener('dragend', onEnd);
865
- document.addEventListener('drop', preventDefault);
695
+ document.addEventListener('drop', preventDefault$1);
866
696
  }
867
697
  function onDrag(event) {
868
698
  const delta = {
@@ -876,7 +706,7 @@ function createDragger(fn, dragPreview) {
876
706
  function onEnd() {
877
707
  document.removeEventListener('dragover', onDrag);
878
708
  document.removeEventListener('dragend', onEnd);
879
- document.removeEventListener('drop', preventDefault);
709
+ document.removeEventListener('drop', preventDefault$1);
880
710
  }
881
711
  return onDragStart;
882
712
  }
@@ -905,10 +735,15 @@ function throttle(fn) {
905
735
  });
906
736
  };
907
737
  }
908
- function preventDefault(event) {
738
+ function preventDefault$1(event) {
909
739
  event.preventDefault();
910
740
  event.stopPropagation();
911
741
  }
742
+ function createEmptyImage() {
743
+ const img = new Image();
744
+ img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
745
+ return img;
746
+ }
912
747
 
913
748
  const DragAndDropContext = createContext({
914
749
  drake: null
@@ -1566,13 +1401,15 @@ class Dragging {
1566
1401
  * @param { import('../../core/FormLayoutValidator').default } formLayoutValidator
1567
1402
  * @param { import('../../core/EventBus').default } eventBus
1568
1403
  * @param { import('../modeling/Modeling').default } modeling
1404
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
1569
1405
  */
1570
- constructor(formFieldRegistry, formLayouter, formLayoutValidator, eventBus, modeling) {
1406
+ constructor(formFieldRegistry, formLayouter, formLayoutValidator, eventBus, modeling, pathRegistry) {
1571
1407
  this._formFieldRegistry = formFieldRegistry;
1572
1408
  this._formLayouter = formLayouter;
1573
1409
  this._formLayoutValidator = formLayoutValidator;
1574
1410
  this._eventBus = eventBus;
1575
1411
  this._modeling = modeling;
1412
+ this._pathRegistry = pathRegistry;
1576
1413
  }
1577
1414
 
1578
1415
  /**
@@ -1606,11 +1443,50 @@ class Dragging {
1606
1443
  const targetRow = this._formLayouter.getRow(target.dataset.rowId);
1607
1444
  let columns;
1608
1445
  let formField;
1446
+ let targetParentId;
1609
1447
  if (formFieldNode) {
1610
1448
  formField = this._formFieldRegistry.get(formFieldNode.dataset.id);
1611
1449
  columns = (formField.layout || {}).columns;
1450
+
1451
+ // (1) check for row constraints
1452
+ if (isRow(target)) {
1453
+ targetParentId = getFormParent(target).dataset.id;
1454
+ const rowError = this._formLayoutValidator.validateField(formField, columns, targetRow);
1455
+ if (rowError) {
1456
+ return rowError;
1457
+ }
1458
+ } else {
1459
+ targetParentId = target.dataset.id;
1460
+ }
1461
+
1462
+ // (2) check target is a valid parent
1463
+ if (!targetParentId) {
1464
+ return 'Drop is not a valid target';
1465
+ }
1466
+
1467
+ // (3) check for path collisions
1468
+ const targetParentFormField = this._formFieldRegistry.get(targetParentId);
1469
+ const currentParentFormField = this._formFieldRegistry.get(formField._parent);
1470
+ if (targetParentFormField !== currentParentFormField) {
1471
+ const targetParentPath = this._pathRegistry.getValuePath(targetParentFormField);
1472
+ const currentParentPath = this._pathRegistry.getValuePath(currentParentFormField);
1473
+ if (targetParentPath.join('.') !== currentParentPath.join('.')) {
1474
+ const isDropAllowedByPathRegistry = this._pathRegistry.executeRecursivelyOnFields(formField, ({
1475
+ field,
1476
+ isClosed
1477
+ }) => {
1478
+ const options = {
1479
+ cutoffNode: currentParentFormField.id
1480
+ };
1481
+ const fieldPath = this._pathRegistry.getValuePath(field, options);
1482
+ return this._pathRegistry.canClaimPath([...targetParentPath, ...fieldPath], isClosed);
1483
+ });
1484
+ if (!isDropAllowedByPathRegistry) {
1485
+ return 'Drop not allowed by path registry';
1486
+ }
1487
+ }
1488
+ }
1612
1489
  }
1613
- return this._formLayoutValidator.validateField(formField, columns, targetRow);
1614
1490
  }
1615
1491
  moveField(element, source, targetRow, targetFormField, targetIndex) {
1616
1492
  const formFieldNode = element.querySelector('.fjs-element');
@@ -1628,6 +1504,7 @@ class Dragging {
1628
1504
  };
1629
1505
  attrs = {
1630
1506
  ...attrs,
1507
+ _parent: targetFormField.id,
1631
1508
  layout: {
1632
1509
  row: targetRow ? targetRow.id : this._formLayouter.nextRowId(),
1633
1510
  // enable auto columns
@@ -1661,14 +1538,13 @@ class Dragging {
1661
1538
 
1662
1539
  // (2.1) dropped in existing row
1663
1540
  if (isRow(target)) {
1664
- unsetDropNotAllowed(target);
1665
1541
  targetRow = this._formLayouter.getRow(target.dataset.rowId);
1542
+ }
1666
1543
 
1667
- // validate whether drop is allowed
1668
- const validationError = this.validateDrop(el, target);
1669
- if (validationError) {
1670
- return drake.cancel(true);
1671
- }
1544
+ // (2.2) validate whether drop is allowed
1545
+ const validationError = this.validateDrop(el, target);
1546
+ if (validationError) {
1547
+ return drake.cancel(true);
1672
1548
  }
1673
1549
  drake.remove();
1674
1550
 
@@ -1716,13 +1592,11 @@ class Dragging {
1716
1592
  return !target.classList.contains(DROP_CONTAINER_HORIZONTAL_CLS);
1717
1593
  }
1718
1594
 
1719
- // validate field drop in row
1720
- if (isRow(target)) {
1721
- const validationError = this.validateDrop(el, target);
1722
- if (validationError) {
1723
- // set error feedback to row
1724
- setDropNotAllowed(target);
1725
- }
1595
+ // validate field drop
1596
+ const validationError = this.validateDrop(el, target);
1597
+ if (validationError) {
1598
+ // set error feedback to row
1599
+ setDropNotAllowed(target);
1726
1600
  }
1727
1601
  return !target.classList.contains(DRAG_NO_DROP_CLS);
1728
1602
  },
@@ -1793,7 +1667,7 @@ class Dragging {
1793
1667
  this._eventBus.fire(event, context);
1794
1668
  }
1795
1669
  }
1796
- Dragging.$inject = ['formFieldRegistry', 'formLayouter', 'formLayoutValidator', 'eventBus', 'modeling'];
1670
+ Dragging.$inject = ['formFieldRegistry', 'formLayouter', 'formLayoutValidator', 'eventBus', 'modeling', 'pathRegistry'];
1797
1671
 
1798
1672
  // helper //////////
1799
1673
 
@@ -1851,7 +1725,6 @@ function FieldDragPreview(props) {
1851
1725
 
1852
1726
  const COLUMNS_REGEX = /^cds--col(-lg)?/;
1853
1727
  const ELEMENT_RESIZING_CLS = 'fjs-element-resizing';
1854
- const RESIZE_DRAG_PREVIEW_CLS = 'fjs-resize-drag-preview';
1855
1728
  const GRID_OFFSET_PX = 16;
1856
1729
  function FieldResizer(props) {
1857
1730
  const {
@@ -1890,17 +1763,8 @@ function FieldResizer(props) {
1890
1763
  const target = getElementNode(field);
1891
1764
  const parent = getParent(target);
1892
1765
 
1893
- // create a blank element to use as drag preview
1894
- // ensure it was only created once
1895
- let blankPreview = getDragPreviewImage(parent);
1896
- if (!blankPreview) {
1897
- blankPreview = document.createElement('div');
1898
- blankPreview.classList.add(RESIZE_DRAG_PREVIEW_CLS);
1899
- parent.appendChild(blankPreview);
1900
- }
1901
-
1902
1766
  // initialize drag handler
1903
- const onDragStart = createDragger(onResize, blankPreview);
1767
+ const onDragStart = createDragger$1(onResize);
1904
1768
  onDragStart(event);
1905
1769
 
1906
1770
  // mitigate auto columns on the grid that
@@ -1923,10 +1787,6 @@ function FieldResizer(props) {
1923
1787
  const target = getElementNode(field);
1924
1788
  unsetResizing(target, position);
1925
1789
  context.current.newColumns = null;
1926
-
1927
- // remove blank preview
1928
- const blankPreview = getDragPreviewImage(getParent(target));
1929
- blankPreview.remove();
1930
1790
  };
1931
1791
  if (field.type === 'default') {
1932
1792
  return null;
@@ -1969,9 +1829,6 @@ function getColumnNode(node) {
1969
1829
  function getElementNode(field) {
1970
1830
  return query('.fjs-element[data-id="' + field.id + '"]');
1971
1831
  }
1972
- function getDragPreviewImage(node) {
1973
- return query('.fjs-resize-drag-preview', node);
1974
- }
1975
1832
  function setResizing(node, position) {
1976
1833
  classes(node).add(ELEMENT_RESIZING_CLS + '-' + position);
1977
1834
  }
@@ -1988,7 +1845,10 @@ function ContextPad(props) {
1988
1845
  children: props.children
1989
1846
  });
1990
1847
  }
1991
- function Empty(props) {
1848
+ function Empty() {
1849
+ return null;
1850
+ }
1851
+ function EmptyRoot(props) {
1992
1852
  return jsx("div", {
1993
1853
  class: "fjs-empty-editor",
1994
1854
  children: jsxs("div", {
@@ -2009,12 +1869,17 @@ function Element$1(props) {
2009
1869
  formFieldRegistry = useService$1('formFieldRegistry'),
2010
1870
  modeling = useService$1('modeling'),
2011
1871
  selection = useService$1('selection');
1872
+ const {
1873
+ hoveredId,
1874
+ setHoveredId
1875
+ } = useContext(FormRenderContext);
2012
1876
  const {
2013
1877
  field
2014
1878
  } = props;
2015
1879
  const {
2016
1880
  id,
2017
- type
1881
+ type,
1882
+ showOutline
2018
1883
  } = field;
2019
1884
  const ref = useRef$1();
2020
1885
  function scrollIntoView({
@@ -2052,6 +1917,12 @@ function Element$1(props) {
2052
1917
  if (selection.isSelected(field)) {
2053
1918
  classes.push('fjs-editor-selected');
2054
1919
  }
1920
+ if (showOutline) {
1921
+ classes.push('fjs-outlined');
1922
+ }
1923
+ if (hoveredId === field.id) {
1924
+ classes.push('fjs-editor-hovered');
1925
+ }
2055
1926
  const onRemove = event => {
2056
1927
  event.stopPropagation();
2057
1928
  const parentField = formFieldRegistry.get(field._parent);
@@ -2071,6 +1942,11 @@ function Element$1(props) {
2071
1942
  tabIndex: type === 'default' ? -1 : 0,
2072
1943
  onClick: onClick,
2073
1944
  onKeyPress: onKeyPress,
1945
+ onMouseOver: e => {
1946
+ // @ts-ignore
1947
+ setHoveredId(field.id);
1948
+ e.stopPropagation();
1949
+ },
2074
1950
  ref: ref,
2075
1951
  children: [jsx(DebugColumns, {
2076
1952
  field: field
@@ -2240,14 +2116,18 @@ function FormEditor$1(props) {
2240
2116
  useEffect(() => {
2241
2117
  eventBus.fire('formEditor.rendered');
2242
2118
  }, []);
2243
- const formRenderContext = {
2119
+ const [hoveredId, setHoveredId] = useState(null);
2120
+ const formRenderContext = useMemo(() => ({
2244
2121
  Children,
2245
2122
  Column,
2246
2123
  Element: Element$1,
2247
2124
  Empty,
2248
- Row
2249
- };
2250
- const formContext = {
2125
+ EmptyRoot,
2126
+ Row,
2127
+ hoveredId,
2128
+ setHoveredId
2129
+ }), [hoveredId]);
2130
+ const formContext = useMemo(() => ({
2251
2131
  getService(type, strict = true) {
2252
2132
  // TODO(philippfromme): clean up
2253
2133
  if (type === 'form') {
@@ -2268,7 +2148,7 @@ function FormEditor$1(props) {
2268
2148
  return injector.get(type, strict);
2269
2149
  },
2270
2150
  formId: formEditor._id
2271
- };
2151
+ }), [ariaLabel, formEditor, injector, schema]);
2272
2152
  const onSubmit = useCallback(() => {}, []);
2273
2153
  const onReset = useCallback(() => {}, []);
2274
2154
 
@@ -2294,6 +2174,7 @@ function FormEditor$1(props) {
2294
2174
  children: jsx(FormContext.Provider, {
2295
2175
  value: formContext,
2296
2176
  children: jsx(FormRenderContext.Provider, {
2177
+ // @ts-ignore
2297
2178
  value: formRenderContext,
2298
2179
  children: jsx(FormComponent, {
2299
2180
  onSubmit: onSubmit,
@@ -2431,10 +2312,12 @@ var renderModule = {
2431
2312
  };
2432
2313
 
2433
2314
  var core = {
2434
- __depends__: [importModule, renderModule],
2315
+ __depends__: [renderModule],
2435
2316
  debounce: ['factory', DebounceFactory],
2436
2317
  eventBus: ['type', EventBus],
2318
+ importer: ['type', Importer],
2437
2319
  formFieldRegistry: ['type', FormFieldRegistry],
2320
+ pathRegistry: ['type', PathRegistry],
2438
2321
  formLayouter: ['type', FormLayouter],
2439
2322
  formLayoutValidator: ['type', FormLayoutValidator],
2440
2323
  fieldFactory: ['type', FieldFactory]
@@ -3143,9 +3026,16 @@ function arrayRemove(array, index) {
3143
3026
  }
3144
3027
  function updatePath(formFieldRegistry, formField, index) {
3145
3028
  const parent = formFieldRegistry.get(formField._parent);
3146
- formField._path = [...parent._path, 'components', index];
3029
+ refreshPathsRecursively(formField, [...parent._path, 'components', index]);
3147
3030
  return formField;
3148
3031
  }
3032
+ function refreshPathsRecursively(formField, path) {
3033
+ formField._path = path;
3034
+ const components = formField.components || [];
3035
+ components.forEach((component, index) => {
3036
+ refreshPathsRecursively(component, [...path, 'components', index]);
3037
+ });
3038
+ }
3149
3039
  function updateRow(formField, rowId) {
3150
3040
  formField.layout = {
3151
3041
  ...(formField.layout || {}),
@@ -3179,7 +3069,7 @@ class AddFormFieldHandler {
3179
3069
  // (1) Add new form field
3180
3070
  arrayAdd$1(get(schema, targetPath), targetIndex, formField);
3181
3071
 
3182
- // (2) Update paths of new form field and its siblings
3072
+ // (2) Update internal paths of new form field and its siblings (and their children)
3183
3073
  get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3184
3074
 
3185
3075
  // (3) Add new form field to form field registry
@@ -3204,7 +3094,7 @@ class AddFormFieldHandler {
3204
3094
  // (1) Remove new form field
3205
3095
  arrayRemove(get(schema, targetPath), targetIndex);
3206
3096
 
3207
- // (2) Update paths of new form field and its siblings
3097
+ // (2) Update internal paths of new form field and its siblings (and their children)
3208
3098
  get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3209
3099
 
3210
3100
  // (3) Remove new form field from form field registry
@@ -3289,10 +3179,12 @@ class MoveFormFieldHandler {
3289
3179
  * @constructor
3290
3180
  * @param { import('../../../FormEditor').default } formEditor
3291
3181
  * @param { import('../../../core/FormFieldRegistry').default } formFieldRegistry
3182
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
3292
3183
  */
3293
- constructor(formEditor, formFieldRegistry) {
3184
+ constructor(formEditor, formFieldRegistry, pathRegistry) {
3294
3185
  this._formEditor = formEditor;
3295
3186
  this._formFieldRegistry = formFieldRegistry;
3187
+ this._pathRegistry = pathRegistry;
3296
3188
  }
3297
3189
  execute(context) {
3298
3190
  this.moveFormField(context);
@@ -3345,27 +3237,42 @@ class MoveFormFieldHandler {
3345
3237
  // (2) Move form field
3346
3238
  mutate(get(schema, sourcePath), sourceIndex, targetIndex);
3347
3239
 
3348
- // (3) Update paths of new form field and its siblings
3240
+ // (3) Update internal paths of new form field and its siblings (and their children)
3349
3241
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3350
3242
  } else {
3351
3243
  const formField = get(schema, [...sourcePath, sourceIndex]);
3244
+
3245
+ // (1) Deregister form field (and children) from path registry
3246
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3247
+ field
3248
+ }) => {
3249
+ this._pathRegistry.unclaimPath(this._pathRegistry.getValuePath(field));
3250
+ });
3352
3251
  formField._parent = targetFormField.id;
3353
3252
 
3354
- // (1) Remove form field
3253
+ // (2) Remove form field
3355
3254
  arrayRemove(get(schema, sourcePath), sourceIndex);
3356
3255
 
3357
- // (2) Update paths of siblings
3256
+ // (3) Update internal paths of siblings (and their children)
3358
3257
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3359
3258
  const targetPath = [...targetFormField._path, 'components'];
3360
3259
 
3361
- // (3) Add to row
3260
+ // (4) Add to row
3362
3261
  updateRow(formField, targetRow ? targetRow.id : null);
3363
3262
 
3364
- // (4) Add form field
3263
+ // (5) Add form field
3365
3264
  arrayAdd$1(get(schema, targetPath), targetIndex, formField);
3366
3265
 
3367
- // (5) Update paths of siblings
3266
+ // (6) Update internal paths of siblings (and their children)
3368
3267
  get(schema, targetPath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3268
+
3269
+ // (7) Reregister form field (and children) from path registry
3270
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3271
+ field,
3272
+ isClosed
3273
+ }) => {
3274
+ this._pathRegistry.claimPath(this._pathRegistry.getValuePath(field), isClosed);
3275
+ });
3369
3276
  }
3370
3277
 
3371
3278
  // TODO: Create updater/change support that automatically updates paths and schema on command execution
@@ -3374,7 +3281,7 @@ class MoveFormFieldHandler {
3374
3281
  });
3375
3282
  }
3376
3283
  }
3377
- MoveFormFieldHandler.$inject = ['formEditor', 'formFieldRegistry'];
3284
+ MoveFormFieldHandler.$inject = ['formEditor', 'formFieldRegistry', 'pathRegistry'];
3378
3285
 
3379
3286
  class RemoveFormFieldHandler {
3380
3287
  /**
@@ -3400,11 +3307,11 @@ class RemoveFormFieldHandler {
3400
3307
  // (1) Remove form field
3401
3308
  arrayRemove(get(schema, sourcePath), sourceIndex);
3402
3309
 
3403
- // (2) Update paths of its siblings
3310
+ // (2) Update internal paths of its siblings (and their children)
3404
3311
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3405
3312
 
3406
- // (3) Remove form field from form field registry
3407
- this._formFieldRegistry.remove(formField);
3313
+ // (3) Remove form field and children from form field registry
3314
+ runRecursively(formField, formField => this._formFieldRegistry.remove(formField));
3408
3315
 
3409
3316
  // TODO: Create updater/change support that automatically updates paths and schema on command execution
3410
3317
  this._formEditor._setState({
@@ -3425,11 +3332,11 @@ class RemoveFormFieldHandler {
3425
3332
  // (1) Add form field
3426
3333
  arrayAdd$1(get(schema, sourcePath), sourceIndex, formField);
3427
3334
 
3428
- // (2) Update paths of its siblings
3335
+ // (2) Update internal paths of its siblings (and their children)
3429
3336
  get(schema, sourcePath).forEach((formField, index) => updatePath(this._formFieldRegistry, formField, index));
3430
3337
 
3431
- // (3) Add form field to form field registry
3432
- this._formFieldRegistry.add(formField);
3338
+ // (3) Add form field and children to form field registry
3339
+ runRecursively(formField, formField => this._formFieldRegistry.add(formField));
3433
3340
 
3434
3341
  // TODO: Create updater/change support that automatically updates paths and schema on command execution
3435
3342
  this._formEditor._setState({
@@ -3477,10 +3384,10 @@ UpdateIdClaimHandler.$inject = ['formFieldRegistry'];
3477
3384
  class UpdateKeyClaimHandler {
3478
3385
  /**
3479
3386
  * @constructor
3480
- * @param { import('../../../core/FormFieldRegistry').default } formFieldRegistry
3387
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
3481
3388
  */
3482
- constructor(formFieldRegistry) {
3483
- this._formFieldRegistry = formFieldRegistry;
3389
+ constructor(pathRegistry) {
3390
+ this._pathRegistry = pathRegistry;
3484
3391
  }
3485
3392
  execute(context) {
3486
3393
  const {
@@ -3488,26 +3395,106 @@ class UpdateKeyClaimHandler {
3488
3395
  formField,
3489
3396
  key
3490
3397
  } = context;
3398
+ const options = {
3399
+ replacements: {
3400
+ [formField.id]: key
3401
+ }
3402
+ };
3403
+ const valuePath = this._pathRegistry.getValuePath(formField, options);
3491
3404
  if (claiming) {
3492
- this._formFieldRegistry._keys.claim(key, formField);
3405
+ this._pathRegistry.claimPath(valuePath, true);
3493
3406
  } else {
3494
- this._formFieldRegistry._keys.unclaim(key);
3407
+ this._pathRegistry.unclaimPath(valuePath);
3495
3408
  }
3409
+
3410
+ // cache path for revert
3411
+ context.valuePath = valuePath;
3496
3412
  }
3497
3413
  revert(context) {
3414
+ const {
3415
+ claiming,
3416
+ valuePath
3417
+ } = context;
3418
+ if (claiming) {
3419
+ this._pathRegistry.unclaimPath(valuePath);
3420
+ } else {
3421
+ this._pathRegistry.claimPath(valuePath, true);
3422
+ }
3423
+ }
3424
+ }
3425
+ UpdateKeyClaimHandler.$inject = ['pathRegistry'];
3426
+
3427
+ class UpdatePathClaimHandler {
3428
+ /**
3429
+ * @constructor
3430
+ * @param { import('@bpmn-io/form-js-viewer').PathRegistry } pathRegistry
3431
+ */
3432
+ constructor(pathRegistry) {
3433
+ this._pathRegistry = pathRegistry;
3434
+ }
3435
+ execute(context) {
3498
3436
  const {
3499
3437
  claiming,
3500
3438
  formField,
3501
- key
3439
+ path
3440
+ } = context;
3441
+ const options = {
3442
+ replacements: {
3443
+ [formField.id]: path
3444
+ }
3445
+ };
3446
+ const valuePaths = [];
3447
+ if (claiming) {
3448
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3449
+ field,
3450
+ isClosed
3451
+ }) => {
3452
+ const valuePath = this._pathRegistry.getValuePath(field, options);
3453
+ valuePaths.push({
3454
+ valuePath,
3455
+ isClosed
3456
+ });
3457
+ this._pathRegistry.claimPath(valuePath, isClosed);
3458
+ });
3459
+ } else {
3460
+ this._pathRegistry.executeRecursivelyOnFields(formField, ({
3461
+ field,
3462
+ isClosed
3463
+ }) => {
3464
+ const valuePath = this._pathRegistry.getValuePath(field, options);
3465
+ valuePaths.push({
3466
+ valuePath,
3467
+ isClosed
3468
+ });
3469
+ this._pathRegistry.unclaimPath(valuePath);
3470
+ });
3471
+ }
3472
+
3473
+ // cache path info for revert
3474
+ context.valuePaths = valuePaths;
3475
+ }
3476
+ revert(context) {
3477
+ const {
3478
+ claiming,
3479
+ valuePaths
3502
3480
  } = context;
3503
3481
  if (claiming) {
3504
- this._formFieldRegistry._keys.unclaim(key);
3482
+ valuePaths.forEach(({
3483
+ valuePath
3484
+ }) => {
3485
+ this._pathRegistry.unclaimPath(valuePath);
3486
+ });
3505
3487
  } else {
3506
- this._formFieldRegistry._keys.claim(key, formField);
3488
+ valuePaths.forEach(({
3489
+ valuePath,
3490
+ isClosed
3491
+ }) => {
3492
+ this._pathRegistry.claimPath(valuePath, isClosed);
3493
+ });
3507
3494
  }
3508
3495
  }
3509
3496
  }
3510
- UpdateKeyClaimHandler.$inject = ['formFieldRegistry'];
3497
+ UpdatePathClaimHandler.$inject = ['pathRegistry'];
3511
3498
 
3512
3499
  class Modeling {
3513
3500
  constructor(commandStack, eventBus, formEditor, formFieldRegistry, fieldFactory) {
@@ -3531,7 +3518,8 @@ class Modeling {
3531
3518
  'formField.move': MoveFormFieldHandler,
3532
3519
  'formField.remove': RemoveFormFieldHandler,
3533
3520
  'id.updateClaim': UpdateIdClaimHandler,
3534
- 'key.updateClaim': UpdateKeyClaimHandler
3521
+ 'key.updateClaim': UpdateKeyClaimHandler,
3522
+ 'path.updateClaim': UpdatePathClaimHandler
3535
3523
  };
3536
3524
  }
3537
3525
  addFormField(attrs, targetFormField, targetIndex) {
@@ -3608,6 +3596,22 @@ class Modeling {
3608
3596
  };
3609
3597
  this._commandStack.execute('key.updateClaim', context);
3610
3598
  }
3599
+ claimPath(formField, path) {
3600
+ const context = {
3601
+ formField,
3602
+ path,
3603
+ claiming: true
3604
+ };
3605
+ this._commandStack.execute('path.updateClaim', context);
3606
+ }
3607
+ unclaimPath(formField, path) {
3608
+ const context = {
3609
+ formField,
3610
+ path,
3611
+ claiming: false
3612
+ };
3613
+ this._commandStack.execute('path.updateClaim', context);
3614
+ }
3611
3615
  }
3612
3616
  Modeling.$inject = ['commandStack', 'eventBus', 'formEditor', 'formFieldRegistry', 'fieldFactory'];
3613
3617
 
@@ -3880,8 +3884,6 @@ FormLayoutUpdater.$inject = ['eventBus', 'formLayouter', 'modeling', 'formEditor
3880
3884
  class IdBehavior extends CommandInterceptor {
3881
3885
  constructor(eventBus, modeling) {
3882
3886
  super(eventBus);
3883
-
3884
- // @ts-ignore-next-line
3885
3887
  this.preExecute('formField.remove', function (context) {
3886
3888
  const {
3887
3889
  formField
@@ -3891,8 +3893,6 @@ class IdBehavior extends CommandInterceptor {
3891
3893
  } = formField;
3892
3894
  modeling.unclaimId(formField, id);
3893
3895
  }, true);
3894
-
3895
- // @ts-ignore-next-line
3896
3896
  this.preExecute('formField.edit', function (context) {
3897
3897
  const {
3898
3898
  formField,
@@ -3908,36 +3908,82 @@ class IdBehavior extends CommandInterceptor {
3908
3908
  IdBehavior.$inject = ['eventBus', 'modeling'];
3909
3909
 
3910
3910
  class KeyBehavior extends CommandInterceptor {
3911
- constructor(eventBus, modeling) {
3911
+ constructor(eventBus, modeling, formFields) {
3912
3912
  super(eventBus);
3913
-
3914
- // @ts-ignore-next-line
3915
3913
  this.preExecute('formField.remove', function (context) {
3916
3914
  const {
3917
3915
  formField
3918
3916
  } = context;
3919
3917
  const {
3920
- key
3918
+ key,
3919
+ type
3921
3920
  } = formField;
3922
- if (key) {
3921
+ const {
3922
+ config
3923
+ } = formFields.get(type);
3924
+ if (config.keyed) {
3923
3925
  modeling.unclaimKey(formField, key);
3924
3926
  }
3925
3927
  }, true);
3926
-
3927
- // @ts-ignore-next-line
3928
3928
  this.preExecute('formField.edit', function (context) {
3929
3929
  const {
3930
3930
  formField,
3931
3931
  properties
3932
3932
  } = context;
3933
- if ('key' in properties) {
3934
- modeling.unclaimKey(formField, formField.key);
3933
+ const {
3934
+ key,
3935
+ type
3936
+ } = formField;
3937
+ const {
3938
+ config
3939
+ } = formFields.get(type);
3940
+ if (config.keyed && 'key' in properties) {
3941
+ modeling.unclaimKey(formField, key);
3935
3942
  modeling.claimKey(formField, properties.key);
3936
3943
  }
3937
3944
  }, true);
3938
3945
  }
3939
3946
  }
3940
- KeyBehavior.$inject = ['eventBus', 'modeling'];
3947
+ KeyBehavior.$inject = ['eventBus', 'modeling', 'formFields'];
3948
+
3949
+ class PathBehavior extends CommandInterceptor {
3950
+ constructor(eventBus, modeling, formFields) {
3951
+ super(eventBus);
3952
+ this.preExecute('formField.remove', function (context) {
3953
+ const {
3954
+ formField
3955
+ } = context;
3956
+ const {
3957
+ path,
3958
+ type
3959
+ } = formField;
3960
+ const {
3961
+ config
3962
+ } = formFields.get(type);
3963
+ if (config.pathed) {
3964
+ modeling.unclaimPath(formField, path);
3965
+ }
3966
+ }, true);
3967
+ this.preExecute('formField.edit', function (context) {
3968
+ const {
3969
+ formField,
3970
+ properties
3971
+ } = context;
3972
+ const {
3973
+ path,
3974
+ type
3975
+ } = formField;
3976
+ const {
3977
+ config
3978
+ } = formFields.get(type);
3979
+ if (config.pathed && 'path' in properties) {
3980
+ modeling.unclaimPath(formField, path);
3981
+ modeling.claimPath(formField, properties.path);
3982
+ }
3983
+ }, true);
3984
+ }
3985
+ }
3986
+ PathBehavior.$inject = ['eventBus', 'modeling', 'formFields'];
3941
3987
 
3942
3988
  class ValidateBehavior extends CommandInterceptor {
3943
3989
  constructor(eventBus) {
@@ -3946,7 +3992,6 @@ class ValidateBehavior extends CommandInterceptor {
3946
3992
  /**
3947
3993
  * Remove custom validation if <validationType> is about to be added.
3948
3994
  */
3949
- // @ts-ignore-next-line
3950
3995
  this.preExecute('formField.edit', function (context) {
3951
3996
  const {
3952
3997
  properties
@@ -3969,9 +4014,10 @@ class ValidateBehavior extends CommandInterceptor {
3969
4014
  ValidateBehavior.$inject = ['eventBus'];
3970
4015
 
3971
4016
  var behaviorModule = {
3972
- __init__: ['idBehavior', 'keyBehavior', 'validateBehavior'],
4017
+ __init__: ['idBehavior', 'keyBehavior', 'pathBehavior', 'validateBehavior'],
3973
4018
  idBehavior: ['type', IdBehavior],
3974
4019
  keyBehavior: ['type', KeyBehavior],
4020
+ pathBehavior: ['type', PathBehavior],
3975
4021
  validateBehavior: ['type', ValidateBehavior]
3976
4022
  };
3977
4023
 
@@ -4642,6 +4688,33 @@ DeleteIcon.defaultProps = {
4642
4688
  width: "16",
4643
4689
  height: "16"
4644
4690
  };
4691
+ var DragIcon = function DragIcon(props) {
4692
+ return jsxs("svg", {
4693
+ ...props,
4694
+ children: [jsx("path", {
4695
+ fill: "#fff",
4696
+ style: {
4697
+ mixBlendMode: "multiply"
4698
+ },
4699
+ d: "M0 0h16v16H0z"
4700
+ }), jsx("path", {
4701
+ fill: "#fff",
4702
+ style: {
4703
+ mixBlendMode: "multiply"
4704
+ },
4705
+ d: "M0 0h16v16H0z"
4706
+ }), jsx("path", {
4707
+ d: "M7 3H5v2h2V3zm4 0H9v2h2V3zM7 7H5v2h2V7zm4 0H9v2h2V7zm-4 4H5v2h2v-2zm4 0H9v2h2v-2z",
4708
+ fill: "#161616"
4709
+ })]
4710
+ });
4711
+ };
4712
+ DragIcon.defaultProps = {
4713
+ width: "16",
4714
+ height: "16",
4715
+ fill: "none",
4716
+ xmlns: "http://www.w3.org/2000/svg"
4717
+ };
4645
4718
  var ExternalLinkIcon = function ExternalLinkIcon(props) {
4646
4719
  return jsx("svg", {
4647
4720
  ...props,
@@ -4649,7 +4722,7 @@ var ExternalLinkIcon = function ExternalLinkIcon(props) {
4649
4722
  fillRule: "evenodd",
4650
4723
  clipRule: "evenodd",
4651
4724
  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",
4652
- fill: "#818798"
4725
+ fill: "currentcolor"
4653
4726
  })
4654
4727
  });
4655
4728
  };
@@ -4792,7 +4865,7 @@ function TooltipWrapper(props) {
4792
4865
  return jsx(Tooltip, {
4793
4866
  ...props,
4794
4867
  value: value,
4795
- forId: prefixId$8(forId)
4868
+ forId: prefixId$9(forId)
4796
4869
  });
4797
4870
  }
4798
4871
  function Tooltip(props) {
@@ -4910,7 +4983,7 @@ function getTooltipPosition(refElement) {
4910
4983
  function isHovered(element) {
4911
4984
  return element.matches(':hover');
4912
4985
  }
4913
- function prefixId$8(id) {
4986
+ function prefixId$9(id) {
4914
4987
  return `bio-properties-panel-${id}`;
4915
4988
  }
4916
4989
 
@@ -5283,638 +5356,739 @@ function Placeholder(props) {
5283
5356
  })
5284
5357
  });
5285
5358
  }
5286
- const DEFAULT_LAYOUT = {};
5287
- const DEFAULT_DESCRIPTION = {};
5288
- const DEFAULT_TOOLTIP = {};
5359
+ function Description$1(props) {
5360
+ const {
5361
+ element,
5362
+ forId,
5363
+ value
5364
+ } = props;
5365
+ const contextDescription = useDescriptionContext(forId, element);
5366
+ const description = value || contextDescription;
5367
+ if (description) {
5368
+ return jsx("div", {
5369
+ class: "bio-properties-panel-description",
5370
+ children: description
5371
+ });
5372
+ }
5373
+ }
5374
+ const noop$6 = () => {};
5289
5375
 
5290
5376
  /**
5291
- * @typedef { {
5292
- * component: import('preact').Component,
5293
- * id: String,
5294
- * isEdited?: Function
5295
- * } } EntryDefinition
5296
- *
5297
- * @typedef { {
5298
- * autoFocusEntry: String,
5299
- * autoOpen?: Boolean,
5300
- * entries: Array<EntryDefinition>,
5301
- * id: String,
5302
- * label: String,
5303
- * remove: (event: MouseEvent) => void
5304
- * } } ListItemDefinition
5305
- *
5306
- * @typedef { {
5307
- * add: (event: MouseEvent) => void,
5308
- * component: import('preact').Component,
5309
- * element: Object,
5310
- * id: String,
5311
- * items: Array<ListItemDefinition>,
5312
- * label: String,
5313
- * shouldSort?: Boolean,
5314
- * shouldOpen?: Boolean
5315
- * } } ListGroupDefinition
5316
- *
5317
- * @typedef { {
5318
- * component?: import('preact').Component,
5319
- * entries: Array<EntryDefinition>,
5320
- * id: String,
5321
- * label: String,
5322
- * shouldOpen?: Boolean
5323
- * } } GroupDefinition
5324
- *
5325
- * @typedef { {
5326
- * [id: String]: GetDescriptionFunction
5327
- * } } DescriptionConfig
5328
- *
5329
- * @typedef { {
5330
- * [id: String]: GetTooltipFunction
5331
- * } } TooltipConfig
5332
- *
5333
- * @callback { {
5334
- * @param {string} id
5335
- * @param {Object} element
5336
- * @returns {string}
5337
- * } } GetDescriptionFunction
5338
- *
5339
- * @callback { {
5340
- * @param {string} id
5341
- * @param {Object} element
5342
- * @returns {string}
5343
- * } } GetTooltipFunction
5344
- *
5345
- * @typedef { {
5346
- * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
5347
- * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
5348
- * } } PlaceholderProvider
5349
- *
5350
- */
5351
-
5352
- /**
5353
- * A basic properties panel component. Describes *how* content will be rendered, accepts
5354
- * data from implementor to describe *what* will be rendered.
5355
- *
5356
- * @param {Object} props
5357
- * @param {Object|Array} props.element
5358
- * @param {import('./components/Header').HeaderProvider} props.headerProvider
5359
- * @param {PlaceholderProvider} [props.placeholderProvider]
5360
- * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
5361
- * @param {Object} [props.layoutConfig]
5362
- * @param {Function} [props.layoutChanged]
5363
- * @param {DescriptionConfig} [props.descriptionConfig]
5364
- * @param {Function} [props.descriptionLoaded]
5365
- * @param {TooltipConfig} [props.tooltipConfig]
5366
- * @param {Function} [props.tooltipLoaded]
5367
- * @param {Object} [props.eventBus]
5377
+ * Buffer `.focus()` calls while the editor is not initialized.
5378
+ * Set Focus inside when the editor is ready.
5368
5379
  */
5369
- function PropertiesPanel(props) {
5380
+ const useBufferedFocus$1 = function (editor, ref) {
5381
+ const [buffer, setBuffer] = useState(undefined);
5382
+ ref.current = useMemo(() => ({
5383
+ focus: offset => {
5384
+ if (editor) {
5385
+ editor.focus(offset);
5386
+ } else {
5387
+ if (typeof offset === 'undefined') {
5388
+ offset = Infinity;
5389
+ }
5390
+ setBuffer(offset);
5391
+ }
5392
+ }
5393
+ }), [editor]);
5394
+ useEffect(() => {
5395
+ if (typeof buffer !== 'undefined' && editor) {
5396
+ editor.focus(buffer);
5397
+ setBuffer(false);
5398
+ }
5399
+ }, [editor, buffer]);
5400
+ };
5401
+ const CodeEditor$1 = forwardRef((props, ref) => {
5370
5402
  const {
5371
- element,
5372
- headerProvider,
5373
- placeholderProvider,
5374
- groups,
5375
- layoutConfig,
5376
- layoutChanged,
5377
- descriptionConfig,
5378
- descriptionLoaded,
5379
- tooltipConfig,
5380
- tooltipLoaded,
5381
- eventBus
5403
+ onInput,
5404
+ disabled,
5405
+ tooltipContainer,
5406
+ enableGutters,
5407
+ value,
5408
+ onLint = noop$6,
5409
+ onPopupOpen = noop$6,
5410
+ popupOpen,
5411
+ contentAttributes = {},
5412
+ hostLanguage = null,
5413
+ singleLine = false
5382
5414
  } = props;
5383
-
5384
- // set-up layout context
5385
- const [layout, setLayout] = useState(createLayout(layoutConfig));
5386
-
5387
- // react to external changes in the layout config
5388
- useUpdateLayoutEffect(() => {
5389
- const newLayout = createLayout(layoutConfig);
5390
- setLayout(newLayout);
5391
- }, [layoutConfig]);
5415
+ const inputRef = useRef$1();
5416
+ const [editor, setEditor] = useState();
5417
+ const [localValue, setLocalValue] = useState(value || '');
5418
+ useBufferedFocus$1(editor, ref);
5419
+ const handleInput = useStaticCallback(newValue => {
5420
+ onInput(newValue);
5421
+ setLocalValue(newValue);
5422
+ });
5392
5423
  useEffect(() => {
5393
- if (typeof layoutChanged === 'function') {
5394
- layoutChanged(layout);
5424
+ let editor;
5425
+ editor = new FeelersEditor({
5426
+ container: inputRef.current,
5427
+ onChange: handleInput,
5428
+ value: localValue,
5429
+ onLint,
5430
+ contentAttributes,
5431
+ tooltipContainer,
5432
+ enableGutters,
5433
+ hostLanguage,
5434
+ singleLine
5435
+ });
5436
+ setEditor(editor);
5437
+ return () => {
5438
+ onLint([]);
5439
+ inputRef.current.innerHTML = '';
5440
+ setEditor(null);
5441
+ };
5442
+ }, []);
5443
+ useEffect(() => {
5444
+ if (!editor) {
5445
+ return;
5395
5446
  }
5396
- }, [layout, layoutChanged]);
5397
- const getLayoutForKey = (key, defaultValue) => {
5398
- return get(layout, key, defaultValue);
5399
- };
5400
- const setLayoutForKey = (key, config) => {
5401
- const newLayout = assign({}, layout);
5402
- set$1(newLayout, key, config);
5403
- setLayout(newLayout);
5404
- };
5405
- const layoutContext = {
5406
- layout,
5407
- setLayout,
5408
- getLayoutForKey,
5409
- setLayoutForKey
5447
+ if (value === localValue) {
5448
+ return;
5449
+ }
5450
+ editor.setValue(value);
5451
+ setLocalValue(value);
5452
+ }, [value]);
5453
+ const handleClick = () => {
5454
+ ref.current.focus();
5410
5455
  };
5456
+ return jsxs("div", {
5457
+ class: classnames('bio-properties-panel-feelers-editor-container', popupOpen ? 'popupOpen' : null),
5458
+ children: [jsx("div", {
5459
+ class: "bio-properties-panel-feelers-editor__open-popup-placeholder",
5460
+ children: "Opened in editor"
5461
+ }), jsx("div", {
5462
+ name: props.name,
5463
+ class: classnames('bio-properties-panel-feelers-editor bio-properties-panel-input', localValue ? 'edited' : null, disabled ? 'disabled' : null),
5464
+ ref: inputRef,
5465
+ onClick: handleClick
5466
+ }), jsx("button", {
5467
+ title: "Open pop-up editor",
5468
+ class: "bio-properties-panel-open-feel-popup",
5469
+ onClick: () => onPopupOpen('feelers'),
5470
+ children: jsx(ExternalLinkIcon, {})
5471
+ })]
5472
+ });
5473
+ });
5474
+ const noop$5 = () => {};
5411
5475
 
5412
- // set-up description context
5413
- const description = useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
5476
+ /**
5477
+ * Buffer `.focus()` calls while the editor is not initialized.
5478
+ * Set Focus inside when the editor is ready.
5479
+ */
5480
+ const useBufferedFocus = function (editor, ref) {
5481
+ const [buffer, setBuffer] = useState(undefined);
5482
+ ref.current = useMemo(() => ({
5483
+ focus: offset => {
5484
+ if (editor) {
5485
+ editor.focus(offset);
5486
+ } else {
5487
+ if (typeof offset === 'undefined') {
5488
+ offset = Infinity;
5489
+ }
5490
+ setBuffer(offset);
5491
+ }
5492
+ }
5493
+ }), [editor]);
5414
5494
  useEffect(() => {
5415
- if (typeof descriptionLoaded === 'function') {
5416
- descriptionLoaded(description);
5495
+ if (typeof buffer !== 'undefined' && editor) {
5496
+ editor.focus(buffer);
5497
+ setBuffer(false);
5417
5498
  }
5418
- }, [description, descriptionLoaded]);
5419
- const getDescriptionForId = (id, element) => {
5420
- return description[id] && description[id](element);
5421
- };
5422
- const descriptionContext = {
5423
- description,
5424
- getDescriptionForId
5425
- };
5499
+ }, [editor, buffer]);
5500
+ };
5501
+ const CodeEditor = forwardRef((props, ref) => {
5502
+ const {
5503
+ enableGutters,
5504
+ value,
5505
+ onInput,
5506
+ onFeelToggle = noop$5,
5507
+ onLint = noop$5,
5508
+ onPopupOpen = noop$5,
5509
+ popupOpen,
5510
+ disabled,
5511
+ tooltipContainer,
5512
+ variables
5513
+ } = props;
5514
+ const inputRef = useRef$1();
5515
+ const [editor, setEditor] = useState();
5516
+ const [localValue, setLocalValue] = useState(value || '');
5517
+ useBufferedFocus(editor, ref);
5518
+ const handleInput = useStaticCallback(newValue => {
5519
+ onInput(newValue);
5520
+ setLocalValue(newValue);
5521
+ });
5522
+ useEffect(() => {
5523
+ let editor;
5426
5524
 
5427
- // set-up tooltip context
5428
- const tooltip = useMemo(() => createTooltipContext(tooltipConfig), [tooltipConfig]);
5525
+ /* Trigger FEEL toggle when
5526
+ *
5527
+ * - `backspace` is pressed
5528
+ * - AND the cursor is at the beginning of the input
5529
+ */
5530
+ const onKeyDown = e => {
5531
+ if (e.key !== 'Backspace' || !editor) {
5532
+ return;
5533
+ }
5534
+ const selection = editor.getSelection();
5535
+ const range = selection.ranges[selection.mainIndex];
5536
+ if (range.from === 0 && range.to === 0) {
5537
+ onFeelToggle();
5538
+ }
5539
+ };
5540
+ editor = new FeelEditor({
5541
+ container: inputRef.current,
5542
+ onChange: handleInput,
5543
+ onKeyDown: onKeyDown,
5544
+ onLint: onLint,
5545
+ tooltipContainer: tooltipContainer,
5546
+ value: localValue,
5547
+ variables: variables,
5548
+ extensions: [...(enableGutters ? [lineNumbers()] : [])]
5549
+ });
5550
+ setEditor(editor);
5551
+ return () => {
5552
+ onLint([]);
5553
+ inputRef.current.innerHTML = '';
5554
+ setEditor(null);
5555
+ };
5556
+ }, []);
5429
5557
  useEffect(() => {
5430
- if (typeof tooltipLoaded === 'function') {
5431
- tooltipLoaded(tooltip);
5558
+ if (!editor) {
5559
+ return;
5432
5560
  }
5433
- }, [tooltip, tooltipLoaded]);
5434
- const getTooltipForId = (id, element) => {
5435
- return tooltip[id] && tooltip[id](element);
5436
- };
5437
- const tooltipContext = {
5438
- tooltip,
5439
- getTooltipForId
5440
- };
5441
- const [errors, setErrors] = useState({});
5442
- const onSetErrors = ({
5443
- errors
5444
- }) => setErrors(errors);
5445
- useEvent('propertiesPanel.setErrors', onSetErrors, eventBus);
5446
- const errorsContext = {
5447
- errors
5448
- };
5449
- const eventContext = {
5450
- eventBus
5451
- };
5452
- const propertiesPanelContext = {
5453
- element
5561
+ if (value === localValue) {
5562
+ return;
5563
+ }
5564
+ editor.setValue(value);
5565
+ setLocalValue(value);
5566
+ }, [value]);
5567
+ useEffect(() => {
5568
+ if (!editor) {
5569
+ return;
5570
+ }
5571
+ editor.setVariables(variables);
5572
+ }, [variables]);
5573
+ const handleClick = () => {
5574
+ ref.current.focus();
5454
5575
  };
5455
-
5456
- // empty state
5457
- if (placeholderProvider && !element) {
5458
- return jsx(Placeholder, {
5459
- ...placeholderProvider.getEmpty()
5460
- });
5461
- }
5462
-
5463
- // multiple state
5464
- if (placeholderProvider && isArray(element)) {
5465
- return jsx(Placeholder, {
5466
- ...placeholderProvider.getMultiple()
5467
- });
5576
+ return jsxs("div", {
5577
+ class: classnames('bio-properties-panel-feel-editor-container', disabled ? 'disabled' : null, popupOpen ? 'popupOpen' : null),
5578
+ children: [jsx("div", {
5579
+ class: "bio-properties-panel-feel-editor__open-popup-placeholder",
5580
+ children: "Opened in editor"
5581
+ }), jsx("div", {
5582
+ name: props.name,
5583
+ class: classnames('bio-properties-panel-input', localValue ? 'edited' : null),
5584
+ ref: inputRef,
5585
+ onClick: handleClick
5586
+ }), jsx("button", {
5587
+ title: "Open pop-up editor",
5588
+ class: "bio-properties-panel-open-feel-popup",
5589
+ onClick: () => onPopupOpen(),
5590
+ children: jsx(ExternalLinkIcon, {})
5591
+ })]
5592
+ });
5593
+ });
5594
+ function FeelIndicator(props) {
5595
+ const {
5596
+ active
5597
+ } = props;
5598
+ if (!active) {
5599
+ return null;
5468
5600
  }
5469
- return jsx(LayoutContext.Provider, {
5470
- value: propertiesPanelContext,
5471
- children: jsx(ErrorsContext.Provider, {
5472
- value: errorsContext,
5473
- children: jsx(DescriptionContext.Provider, {
5474
- value: descriptionContext,
5475
- children: jsx(TooltipContext.Provider, {
5476
- value: tooltipContext,
5477
- children: jsx(LayoutContext.Provider, {
5478
- value: layoutContext,
5479
- children: jsx(EventContext.Provider, {
5480
- value: eventContext,
5481
- children: jsxs("div", {
5482
- class: "bio-properties-panel",
5483
- children: [jsx(Header, {
5484
- element: element,
5485
- headerProvider: headerProvider
5486
- }), jsx("div", {
5487
- class: "bio-properties-panel-scroll-container",
5488
- children: groups.map(group => {
5489
- const {
5490
- component: Component = Group,
5491
- id
5492
- } = group;
5493
- return createElement(Component, {
5494
- ...group,
5495
- key: id,
5496
- element: element
5497
- });
5498
- })
5499
- })]
5500
- })
5501
- })
5502
- })
5503
- })
5504
- })
5505
- })
5601
+ return jsx("span", {
5602
+ class: "bio-properties-panel-feel-indicator",
5603
+ children: "="
5506
5604
  });
5507
5605
  }
5606
+ const noop$4 = () => {};
5508
5607
 
5509
- // helpers //////////////////
5608
+ /**
5609
+ * @param {Object} props
5610
+ * @param {Object} props.label
5611
+ * @param {String} props.feel
5612
+ */
5613
+ function FeelIcon(props) {
5614
+ const {
5615
+ feel = false,
5616
+ active,
5617
+ disabled = false,
5618
+ onClick = noop$4
5619
+ } = props;
5620
+ const feelRequiredLabel = 'FEEL expression is mandatory';
5621
+ const feelOptionalLabel = `Click to ${active ? 'remove' : 'set a'} dynamic value with FEEL expression`;
5622
+ const handleClick = e => {
5623
+ onClick(e);
5510
5624
 
5511
- function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
5512
- return {
5513
- ...defaults,
5514
- ...overrides
5625
+ // when pointer event was created from keyboard, keep focus on button
5626
+ if (!e.pointerType) {
5627
+ e.stopPropagation();
5628
+ }
5515
5629
  };
5630
+ return jsx("button", {
5631
+ class: classnames('bio-properties-panel-feel-icon', active ? 'active' : null, feel === 'required' ? 'required' : 'optional'),
5632
+ onClick: handleClick,
5633
+ disabled: feel === 'required' || disabled,
5634
+ title: feel === 'required' ? feelRequiredLabel : feelOptionalLabel,
5635
+ children: jsx(FeelIcon$1, {})
5636
+ });
5516
5637
  }
5517
- function createDescriptionContext(overrides = {}) {
5518
- return {
5519
- ...DEFAULT_DESCRIPTION,
5520
- ...overrides
5521
- };
5638
+ const FeelPopupContext = createContext({
5639
+ open: () => {},
5640
+ close: () => {},
5641
+ source: null
5642
+ });
5643
+
5644
+ /**
5645
+ * Add a dragger that calls back the passed function with
5646
+ * { event, delta } on drag.
5647
+ *
5648
+ * @example
5649
+ *
5650
+ * function dragMove(event, delta) {
5651
+ * // we are dragging (!!)
5652
+ * }
5653
+ *
5654
+ * domElement.addEventListener('dragstart', dragger(dragMove));
5655
+ *
5656
+ * @param {Function} fn
5657
+ * @param {Element} [dragPreview]
5658
+ *
5659
+ * @return {Function} drag start callback function
5660
+ */
5661
+ function createDragger(fn, dragPreview) {
5662
+ let self;
5663
+ let startX, startY;
5664
+
5665
+ /** drag start */
5666
+ function onDragStart(event) {
5667
+ self = this;
5668
+ startX = event.clientX;
5669
+ startY = event.clientY;
5670
+
5671
+ // (1) prevent preview image
5672
+ if (event.dataTransfer) {
5673
+ event.dataTransfer.setDragImage(dragPreview || emptyCanvas(), 0, 0);
5674
+ }
5675
+
5676
+ // (2) setup drag listeners
5677
+
5678
+ // attach drag + cleanup event
5679
+ document.addEventListener('dragover', onDrag);
5680
+ document.addEventListener('dragend', onEnd);
5681
+ document.addEventListener('drop', preventDefault);
5682
+ }
5683
+ function onDrag(event) {
5684
+ const delta = {
5685
+ x: event.clientX - startX,
5686
+ y: event.clientY - startY
5687
+ };
5688
+
5689
+ // call provided fn with event, delta
5690
+ return fn.call(self, event, delta);
5691
+ }
5692
+ function onEnd() {
5693
+ document.removeEventListener('dragover', onDrag);
5694
+ document.removeEventListener('dragend', onEnd);
5695
+ document.removeEventListener('drop', preventDefault);
5696
+ }
5697
+ return onDragStart;
5522
5698
  }
5523
- function createTooltipContext(overrides = {}) {
5524
- return {
5525
- ...DEFAULT_TOOLTIP,
5526
- ...overrides
5527
- };
5699
+ function preventDefault(event) {
5700
+ event.preventDefault();
5701
+ event.stopPropagation();
5528
5702
  }
5529
-
5530
- // hooks //////////////////
5703
+ function emptyCanvas() {
5704
+ return domify('<canvas width="0" height="0" />');
5705
+ }
5706
+ const noop$3 = () => {};
5531
5707
 
5532
5708
  /**
5533
- * This hook behaves like useLayoutEffect, but does not trigger on the first render.
5709
+ * A generic popup component.
5534
5710
  *
5535
- * @param {Function} effect
5536
- * @param {Array} deps
5711
+ * @param {Object} props
5712
+ * @param {HTMLElement} [props.container]
5713
+ * @param {string} [props.className]
5714
+ * @param {{x: number, y: number}} [props.position]
5715
+ * @param {number} [props.width]
5716
+ * @param {number} [props.height]
5717
+ * @param {Function} props.onClose
5718
+ * @param {Function} [props.onPostActivate]
5719
+ * @param {Function} [props.onPostDeactivate]
5720
+ * @param {boolean} [props.returnFocus]
5721
+ * @param {string} props.title
5537
5722
  */
5538
- function useUpdateLayoutEffect(effect, deps) {
5539
- const isMounted = useRef$1(false);
5540
- useLayoutEffect(() => {
5541
- if (isMounted.current) {
5542
- return effect();
5543
- } else {
5544
- isMounted.current = true;
5723
+ function Popup(props) {
5724
+ const {
5725
+ container,
5726
+ className,
5727
+ position,
5728
+ width,
5729
+ height,
5730
+ onClose,
5731
+ onPostActivate = noop$3,
5732
+ onPostDeactivate = noop$3,
5733
+ returnFocus = true,
5734
+ title
5735
+ } = props;
5736
+ const focusTrapRef = useRef$1(null);
5737
+ const popupRef = useRef$1(null);
5738
+ const handleKeyPress = event => {
5739
+ if (event.key === 'Escape') {
5740
+ onClose();
5545
5741
  }
5546
- }, deps);
5742
+ };
5743
+
5744
+ // re-activate focus trap on focus
5745
+ const handleFocus = () => {
5746
+ if (focusTrapRef.current) {
5747
+ focusTrapRef.current.activate();
5748
+ }
5749
+ };
5750
+ let style = {};
5751
+ if (position) {
5752
+ style = {
5753
+ ...style,
5754
+ top: position.top + 'px',
5755
+ left: position.left + 'px'
5756
+ };
5757
+ }
5758
+ if (width) {
5759
+ style.width = width + 'px';
5760
+ }
5761
+ if (height) {
5762
+ style.height = height + 'px';
5763
+ }
5764
+ useEffect(() => {
5765
+ if (popupRef.current) {
5766
+ popupRef.current.addEventListener('keydown', handleKeyPress);
5767
+ }
5768
+ return () => {
5769
+ popupRef.current.removeEventListener('keydown', handleKeyPress);
5770
+ };
5771
+ }, [popupRef]);
5772
+ useEffect(() => {
5773
+ if (popupRef.current) {
5774
+ popupRef.current.addEventListener('focusin', handleFocus);
5775
+ }
5776
+ return () => {
5777
+ popupRef.current.removeEventListener('focusin', handleFocus);
5778
+ };
5779
+ }, [popupRef]);
5780
+ useEffect(() => {
5781
+ if (popupRef.current) {
5782
+ focusTrapRef.current = focusTrap.createFocusTrap(popupRef.current, {
5783
+ clickOutsideDeactivates: true,
5784
+ fallbackFocus: popupRef.current,
5785
+ onPostActivate,
5786
+ onPostDeactivate,
5787
+ returnFocusOnDeactivate: returnFocus
5788
+ });
5789
+ focusTrapRef.current.activate();
5790
+ }
5791
+ return () => focusTrapRef.current && focusTrapRef.current.deactivate();
5792
+ }, [popupRef]);
5793
+ return createPortal(jsx("div", {
5794
+ "aria-label": title,
5795
+ tabIndex: -1,
5796
+ ref: popupRef,
5797
+ role: "dialog",
5798
+ class: classnames('bio-properties-panel-popup', className),
5799
+ style: style,
5800
+ children: props.children
5801
+ }), container || document.body);
5547
5802
  }
5548
- function CollapsibleEntry(props) {
5803
+ Popup.Title = Title;
5804
+ Popup.Body = Body;
5805
+ Popup.Footer = Footer;
5806
+ function Title(props) {
5549
5807
  const {
5550
- element,
5551
- entries = [],
5552
- id,
5553
- label,
5554
- open: shouldOpen,
5555
- remove
5808
+ children,
5809
+ className,
5810
+ draggable,
5811
+ title,
5812
+ ...rest
5556
5813
  } = props;
5557
- const [open, setOpen] = useState(shouldOpen);
5558
- const toggleOpen = () => setOpen(!open);
5559
- const {
5560
- onShow
5561
- } = useContext(LayoutContext);
5562
- const propertiesPanelContext = {
5563
- ...useContext(LayoutContext),
5564
- onShow: useCallback(() => {
5565
- setOpen(true);
5566
- if (isFunction(onShow)) {
5567
- onShow();
5568
- }
5569
- }, [onShow, setOpen])
5570
- };
5571
5814
 
5572
- // todo(pinussilvestrus): translate once we have a translate mechanism for the core
5573
- const placeholderLabel = '<empty>';
5815
+ // we can't use state as we need to
5816
+ // manipulate this inside dragging events
5817
+ const context = useRef$1({
5818
+ startPosition: null,
5819
+ newPosition: null
5820
+ });
5821
+ const dragPreviewRef = useRef$1();
5822
+ const titleRef = useRef$1();
5823
+ const onMove = throttle$1((_, delta) => {
5824
+ const {
5825
+ x: dx,
5826
+ y: dy
5827
+ } = delta;
5828
+ const newPosition = {
5829
+ x: context.current.startPosition.x + dx,
5830
+ y: context.current.startPosition.y + dy
5831
+ };
5832
+ const popupParent = getPopupParent(titleRef.current);
5833
+ popupParent.style.top = newPosition.y + 'px';
5834
+ popupParent.style.left = newPosition.x + 'px';
5835
+ });
5836
+ const onMoveStart = event => {
5837
+ // initialize drag handler
5838
+ const onDragStart = createDragger(onMove, dragPreviewRef.current);
5839
+ onDragStart(event);
5840
+ const popupParent = getPopupParent(titleRef.current);
5841
+ const bounds = popupParent.getBoundingClientRect();
5842
+ context.current.startPosition = {
5843
+ x: bounds.left,
5844
+ y: bounds.top
5845
+ };
5846
+ };
5847
+ const onMoveEnd = () => {
5848
+ context.current.newPosition = null;
5849
+ };
5574
5850
  return jsxs("div", {
5575
- "data-entry-id": id,
5576
- class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
5577
- children: [jsxs("div", {
5578
- class: "bio-properties-panel-collapsible-entry-header",
5579
- onClick: toggleOpen,
5851
+ class: classnames('bio-properties-panel-popup__header', draggable && 'draggable', className),
5852
+ ref: titleRef,
5853
+ draggable: draggable,
5854
+ onDragStart: onMoveStart,
5855
+ onDragEnd: onMoveEnd,
5856
+ ...rest,
5857
+ children: [draggable && jsxs(Fragment$1, {
5580
5858
  children: [jsx("div", {
5581
- title: label || placeholderLabel,
5582
- class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
5583
- children: label || placeholderLabel
5584
- }), jsx("button", {
5585
- title: "Toggle list item",
5586
- class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
5587
- children: jsx(ArrowIcon, {
5588
- class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
5589
- })
5590
- }), remove ? jsx("button", {
5591
- title: "Delete item",
5592
- class: "bio-properties-panel-remove-entry",
5593
- onClick: remove,
5594
- children: jsx(DeleteIcon, {})
5595
- }) : null]
5859
+ ref: dragPreviewRef,
5860
+ class: "bio-properties-panel-popup__drag-preview"
5861
+ }), jsx("div", {
5862
+ class: "bio-properties-panel-popup__drag-handle",
5863
+ children: jsx(DragIcon, {})
5864
+ })]
5596
5865
  }), jsx("div", {
5597
- class: classnames('bio-properties-panel-collapsible-entry-entries', open ? 'open' : ''),
5598
- children: jsx(LayoutContext.Provider, {
5599
- value: propertiesPanelContext,
5600
- children: entries.map(entry => {
5601
- const {
5602
- component: Component,
5603
- id
5604
- } = entry;
5605
- return createElement(Component, {
5606
- ...entry,
5607
- element: element,
5608
- key: id
5609
- });
5610
- })
5611
- })
5612
- })]
5866
+ class: "bio-properties-panel-popup__title",
5867
+ children: title
5868
+ }), children]
5613
5869
  });
5614
5870
  }
5615
- function ListItem(props) {
5871
+ function Body(props) {
5616
5872
  const {
5617
- autoFocusEntry,
5618
- autoOpen
5873
+ children,
5874
+ className,
5875
+ ...rest
5619
5876
  } = props;
5620
-
5621
- // focus specified entry on auto open
5622
- useEffect(() => {
5623
- if (autoOpen && autoFocusEntry) {
5624
- const entry = query(`[data-entry-id="${autoFocusEntry}"]`);
5625
- const focusableInput = query('.bio-properties-panel-input', entry);
5626
- if (focusableInput) {
5627
- if (isFunction(focusableInput.select)) {
5628
- focusableInput.select();
5629
- } else if (isFunction(focusableInput.focus)) {
5630
- focusableInput.focus();
5631
- }
5632
- }
5633
- }
5634
- }, [autoOpen, autoFocusEntry]);
5635
5877
  return jsx("div", {
5636
- class: "bio-properties-panel-list-item",
5637
- children: jsx(CollapsibleEntry, {
5638
- ...props,
5639
- open: autoOpen
5640
- })
5878
+ class: classnames('bio-properties-panel-popup__body', className),
5879
+ ...rest,
5880
+ children: children
5641
5881
  });
5642
5882
  }
5643
- const noop$3 = () => {};
5883
+ function Footer(props) {
5884
+ const {
5885
+ children,
5886
+ className,
5887
+ ...rest
5888
+ } = props;
5889
+ return jsx("div", {
5890
+ class: classnames('bio-properties-panel-popup__footer', className),
5891
+ ...rest,
5892
+ children: props.children
5893
+ });
5894
+ }
5895
+
5896
+ // helpers //////////////////////
5897
+
5898
+ function getPopupParent(node) {
5899
+ return node.closest('.bio-properties-panel-popup');
5900
+ }
5901
+ const FEEL_POPUP_WIDTH = 700;
5902
+ const FEEL_POPUP_HEIGHT = 250;
5644
5903
 
5645
5904
  /**
5646
- * @param {import('../PropertiesPanel').ListGroupDefinition} props
5905
+ * FEEL popup component, built as a singleton.
5647
5906
  */
5648
- function ListGroup(props) {
5907
+ function FEELPopupRoot(props) {
5649
5908
  const {
5650
- add,
5651
- element,
5652
- id,
5653
- items,
5654
- label,
5655
- shouldOpen = true,
5656
- shouldSort = true
5909
+ element
5657
5910
  } = props;
5658
- const groupRef = useRef$1(null);
5659
- const [open, setOpen] = useLayoutState(['groups', id, 'open'], false);
5660
- const [sticky, setSticky] = useState(false);
5661
- const onShow = useCallback(() => setOpen(true), [setOpen]);
5662
- const [ordering, setOrdering] = useState([]);
5663
- const [newItemAdded, setNewItemAdded] = useState(false);
5664
-
5665
- // Flag to mark that add button was clicked in the last render cycle
5666
- const [addTriggered, setAddTriggered] = useState(false);
5667
- const prevItems = usePrevious(items);
5668
5911
  const prevElement = usePrevious(element);
5669
- const elementChanged = element !== prevElement;
5670
- const shouldHandleEffects = !elementChanged && (shouldSort || shouldOpen);
5671
-
5672
- // reset initial ordering when element changes (before first render)
5673
- if (elementChanged) {
5674
- setOrdering(createOrdering(shouldSort ? sortItems(items) : items));
5675
- }
5676
-
5677
- // keep ordering in sync to items - and open changes
5678
-
5679
- // (0) set initial ordering from given items
5680
- useEffect(() => {
5681
- if (!prevItems || !shouldSort) {
5682
- setOrdering(createOrdering(items));
5683
- }
5684
- }, [items, element]);
5685
-
5686
- // (1) items were added
5912
+ const [popupConfig, setPopupConfig] = useState({});
5913
+ const [open, setOpen] = useState(false);
5914
+ const [source, setSource] = useState(null);
5915
+ const [sourceElement, setSourceElement] = useState(null);
5916
+ const handleOpen = (key, config, _sourceElement) => {
5917
+ setSource(key);
5918
+ setPopupConfig(config);
5919
+ setOpen(true);
5920
+ setSourceElement(_sourceElement);
5921
+ };
5922
+ const handleClose = () => {
5923
+ setOpen(false);
5924
+ setSource(null);
5925
+ };
5926
+ const feelPopupContext = {
5927
+ open: handleOpen,
5928
+ close: handleClose,
5929
+ source
5930
+ };
5931
+
5932
+ // close popup on element change, cf. https://github.com/bpmn-io/properties-panel/issues/270
5687
5933
  useEffect(() => {
5688
- // reset addTriggered flag
5689
- setAddTriggered(false);
5690
- if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
5691
- let add = [];
5692
- items.forEach(item => {
5693
- if (!ordering.includes(item.id)) {
5694
- add.push(item.id);
5695
- }
5696
- });
5697
- let newOrdering = ordering;
5698
-
5699
- // open if not open, configured and triggered by add button
5700
- //
5701
- // TODO(marstamm): remove once we refactor layout handling for listGroups.
5702
- // Ideally, opening should be handled as part of the `add` callback and
5703
- // not be a concern for the ListGroup component.
5704
- if (addTriggered && !open && shouldOpen) {
5705
- toggleOpen();
5706
- }
5707
-
5708
- // filter when not open and configured
5709
- if (!open && shouldSort) {
5710
- newOrdering = createOrdering(sortItems(items));
5711
- }
5712
-
5713
- // add new items on top or bottom depending on sorting behavior
5714
- newOrdering = newOrdering.filter(item => !add.includes(item));
5715
- if (shouldSort) {
5716
- newOrdering.unshift(...add);
5717
- } else {
5718
- newOrdering.push(...add);
5719
- }
5720
- setOrdering(newOrdering);
5721
- setNewItemAdded(addTriggered);
5722
- } else {
5723
- setNewItemAdded(false);
5934
+ if (element && element !== prevElement) {
5935
+ handleClose();
5724
5936
  }
5725
- }, [items, open, shouldHandleEffects, addTriggered]);
5726
-
5727
- // (2) sort items on open if shouldSort is set
5937
+ }, [element]);
5938
+ return jsxs(FeelPopupContext.Provider, {
5939
+ value: feelPopupContext,
5940
+ children: [open && jsx(FeelPopupComponent, {
5941
+ onClose: handleClose,
5942
+ sourceElement: sourceElement,
5943
+ ...popupConfig
5944
+ }), props.children]
5945
+ });
5946
+ }
5947
+ function FeelPopupComponent(props) {
5948
+ const {
5949
+ id,
5950
+ hostLanguage,
5951
+ onInput,
5952
+ onClose,
5953
+ position,
5954
+ singleLine,
5955
+ sourceElement,
5956
+ title,
5957
+ tooltipContainer,
5958
+ type,
5959
+ value,
5960
+ variables
5961
+ } = props;
5962
+ const editorRef = useRef$1();
5963
+ const handleSetReturnFocus = () => {
5964
+ sourceElement && sourceElement.focus();
5965
+ };
5728
5966
  useEffect(() => {
5729
- if (shouldSort && open && !newItemAdded) {
5730
- setOrdering(createOrdering(sortItems(items)));
5731
- }
5732
- }, [open, shouldSort]);
5967
+ const editor = editorRef.current;
5968
+ if (editor) {
5969
+ editor.focus();
5970
+ }
5971
+ }, [editorRef, id]);
5972
+ return jsxs(Popup, {
5973
+ className: "bio-properties-panel-feel-popup",
5974
+ position: position,
5975
+ title: title,
5976
+ onClose: onClose
5977
+
5978
+ // handle focus manually on deactivate
5979
+ ,
5980
+
5981
+ returnFocus: false,
5982
+ onPostDeactivate: handleSetReturnFocus,
5983
+ height: FEEL_POPUP_HEIGHT,
5984
+ width: FEEL_POPUP_WIDTH,
5985
+ children: [jsx(Popup.Title, {
5986
+ title: title,
5987
+ draggable: true
5988
+ }), jsx(Popup.Body, {
5989
+ children: jsxs("div", {
5990
+ class: "bio-properties-panel-feel-popup__body",
5991
+ children: [type === 'feel' && jsx(CodeEditor, {
5992
+ enableGutters: true,
5993
+ id: prefixId$8(id),
5994
+ name: id,
5995
+ onInput: onInput,
5996
+ value: value,
5997
+ variables: variables,
5998
+ ref: editorRef,
5999
+ tooltipContainer: tooltipContainer
6000
+ }), type === 'feelers' && jsx(CodeEditor$1, {
6001
+ id: prefixId$8(id),
6002
+ contentAttributes: {
6003
+ 'aria-label': title
6004
+ },
6005
+ enableGutters: true,
6006
+ hostLanguage: hostLanguage,
6007
+ name: id,
6008
+ onInput: onInput,
6009
+ value: value,
6010
+ ref: editorRef,
6011
+ singleLine: singleLine,
6012
+ tooltipContainer: tooltipContainer
6013
+ })]
6014
+ })
6015
+ }), jsx(Popup.Footer, {
6016
+ children: jsx("button", {
6017
+ onClick: onClose,
6018
+ title: "Close pop-up editor",
6019
+ class: "bio-properties-panel-feel-popup__close-btn",
6020
+ children: "Close"
6021
+ })
6022
+ })]
6023
+ });
6024
+ }
5733
6025
 
5734
- // (3) items were deleted
5735
- useEffect(() => {
5736
- if (shouldHandleEffects && prevItems && items.length < prevItems.length) {
5737
- let keep = [];
5738
- ordering.forEach(o => {
5739
- if (getItem(items, o)) {
5740
- keep.push(o);
5741
- }
5742
- });
5743
- setOrdering(keep);
5744
- }
5745
- }, [items, shouldHandleEffects]);
6026
+ // helpers /////////////////
5746
6027
 
5747
- // set css class when group is sticky to top
5748
- useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);
5749
- const toggleOpen = () => setOpen(!open);
5750
- const hasItems = !!items.length;
5751
- const propertiesPanelContext = {
5752
- ...useContext(LayoutContext),
5753
- onShow
6028
+ function prefixId$8(id) {
6029
+ return `bio-properties-panel-${id}`;
6030
+ }
6031
+ function ToggleSwitch(props) {
6032
+ const {
6033
+ id,
6034
+ label,
6035
+ onInput,
6036
+ value,
6037
+ switcherLabel,
6038
+ inline,
6039
+ onFocus,
6040
+ onBlur,
6041
+ inputRef,
6042
+ tooltip
6043
+ } = props;
6044
+ const [localValue, setLocalValue] = useState(value);
6045
+ const handleInputCallback = async () => {
6046
+ onInput(!value);
5754
6047
  };
5755
- const handleAddClick = e => {
5756
- setAddTriggered(true);
5757
- add(e);
6048
+ const handleInput = e => {
6049
+ handleInputCallback();
6050
+ setLocalValue(e.target.value);
5758
6051
  };
5759
- const allErrors = useErrors();
5760
- const hasError = items.some(item => {
5761
- if (allErrors[item.id]) {
5762
- return true;
5763
- }
5764
- if (!item.entries) {
6052
+ useEffect(() => {
6053
+ if (value === localValue) {
5765
6054
  return;
5766
6055
  }
5767
-
5768
- // also check if the error is nested, e.g. for name-value entries
5769
- return item.entries.some(entry => allErrors[entry.id]);
5770
- });
6056
+ setLocalValue(value);
6057
+ }, [value]);
5771
6058
  return jsxs("div", {
5772
- class: "bio-properties-panel-group",
5773
- "data-group-id": 'group-' + id,
5774
- ref: groupRef,
5775
- children: [jsxs("div", {
5776
- class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
5777
- onClick: hasItems ? toggleOpen : noop$3,
5778
- children: [jsx("div", {
5779
- title: label,
5780
- class: "bio-properties-panel-group-header-title",
5781
- children: jsx(TooltipWrapper, {
5782
- value: props.tooltip,
5783
- forId: 'group-' + id,
5784
- element: element,
5785
- parent: groupRef,
5786
- children: label
5787
- })
5788
- }), jsxs("div", {
5789
- class: "bio-properties-panel-group-header-buttons",
5790
- children: [add ? jsxs("button", {
5791
- title: "Create new list item",
5792
- class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
5793
- onClick: handleAddClick,
5794
- children: [jsx(CreateIcon, {}), !hasItems ? jsx("span", {
5795
- class: "bio-properties-panel-add-entry-label",
5796
- children: "Create"
5797
- }) : null]
5798
- }) : null, hasItems ? jsx("div", {
5799
- title: `List contains ${items.length} item${items.length != 1 ? 's' : ''}`,
5800
- class: classnames('bio-properties-panel-list-badge', hasError ? 'bio-properties-panel-list-badge--error' : ''),
5801
- children: items.length
5802
- }) : null, hasItems ? jsx("button", {
5803
- title: "Toggle section",
5804
- class: "bio-properties-panel-group-header-button bio-properties-panel-arrow",
5805
- children: jsx(ArrowIcon, {
5806
- class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
5807
- })
5808
- }) : null]
5809
- })]
5810
- }), jsx("div", {
5811
- class: classnames('bio-properties-panel-list', open && hasItems ? 'open' : ''),
5812
- children: jsx(LayoutContext.Provider, {
5813
- value: propertiesPanelContext,
5814
- children: ordering.map((o, index) => {
5815
- const item = getItem(items, o);
5816
- if (!item) {
5817
- return;
5818
- }
5819
- const {
5820
- id
5821
- } = item;
5822
-
5823
- // if item was added, open it
5824
- // Existing items will not be affected as autoOpen is only applied on first render
5825
- const autoOpen = newItemAdded;
5826
- return createElement(ListItem, {
5827
- ...item,
5828
- autoOpen: autoOpen,
5829
- element: element,
5830
- index: index,
5831
- key: id
5832
- });
5833
- })
5834
- })
5835
- })]
5836
- });
5837
- }
5838
-
5839
- // helpers ////////////////////
5840
-
5841
- /**
5842
- * Sorts given items alphanumeric by label
5843
- */
5844
- function sortItems(items) {
5845
- return sortBy(items, i => i.label.toLowerCase());
5846
- }
5847
- function getItem(items, id) {
5848
- return find(items, i => i.id === id);
5849
- }
5850
- function createOrdering(items) {
5851
- return items.map(i => i.id);
5852
- }
5853
- function Description$1(props) {
5854
- const {
5855
- element,
5856
- forId,
5857
- value
5858
- } = props;
5859
- const contextDescription = useDescriptionContext(forId, element);
5860
- const description = value || contextDescription;
5861
- if (description) {
5862
- return jsx("div", {
5863
- class: "bio-properties-panel-description",
5864
- children: description
5865
- });
5866
- }
5867
- }
5868
- function Checkbox(props) {
5869
- const {
5870
- id,
5871
- label,
5872
- onChange,
5873
- disabled,
5874
- value = false,
5875
- onFocus,
5876
- onBlur,
5877
- tooltip
5878
- } = props;
5879
- const [localValue, setLocalValue] = useState(value);
5880
- const handleChangeCallback = ({
5881
- target
5882
- }) => {
5883
- onChange(target.checked);
5884
- };
5885
- const handleChange = e => {
5886
- handleChangeCallback(e);
5887
- setLocalValue(e.target.value);
5888
- };
5889
- useEffect(() => {
5890
- if (value === localValue) {
5891
- return;
5892
- }
5893
- setLocalValue(value);
5894
- }, [value]);
5895
- const ref = useShowEntryEvent(id);
5896
- return jsxs("div", {
5897
- class: "bio-properties-panel-checkbox",
5898
- children: [jsx("input", {
5899
- ref: ref,
5900
- id: prefixId$7(id),
5901
- name: id,
5902
- onFocus: onFocus,
5903
- onBlur: onBlur,
5904
- type: "checkbox",
5905
- class: "bio-properties-panel-input",
5906
- onChange: handleChange,
5907
- checked: localValue,
5908
- disabled: disabled
5909
- }), jsx("label", {
5910
- for: prefixId$7(id),
6059
+ class: classnames('bio-properties-panel-toggle-switch', {
6060
+ inline
6061
+ }),
6062
+ children: [jsx("label", {
5911
6063
  class: "bio-properties-panel-label",
6064
+ for: prefixId$7(id),
5912
6065
  children: jsx(TooltipWrapper, {
5913
6066
  value: tooltip,
5914
6067
  forId: id,
5915
6068
  element: props.element,
5916
6069
  children: label
5917
6070
  })
6071
+ }), jsxs("div", {
6072
+ class: "bio-properties-panel-field-wrapper",
6073
+ children: [jsxs("label", {
6074
+ class: "bio-properties-panel-toggle-switch__switcher",
6075
+ children: [jsx("input", {
6076
+ ref: inputRef,
6077
+ id: prefixId$7(id),
6078
+ class: "bio-properties-panel-input",
6079
+ type: "checkbox",
6080
+ onFocus: onFocus,
6081
+ onBlur: onBlur,
6082
+ name: id,
6083
+ onInput: handleInput,
6084
+ checked: !!localValue
6085
+ }), jsx("span", {
6086
+ class: "bio-properties-panel-toggle-switch__slider"
6087
+ })]
6088
+ }), switcherLabel && jsx("p", {
6089
+ class: "bio-properties-panel-toggle-switch__label",
6090
+ children: switcherLabel
6091
+ })]
5918
6092
  })]
5919
6093
  });
5920
6094
  }
@@ -5925,44 +6099,43 @@ function Checkbox(props) {
5925
6099
  * @param {String} props.id
5926
6100
  * @param {String} props.description
5927
6101
  * @param {String} props.label
6102
+ * @param {String} props.switcherLabel
6103
+ * @param {Boolean} props.inline
5928
6104
  * @param {Function} props.getValue
5929
6105
  * @param {Function} props.setValue
5930
6106
  * @param {Function} props.onFocus
5931
6107
  * @param {Function} props.onBlur
5932
6108
  * @param {string|import('preact').Component} props.tooltip
5933
- * @param {boolean} [props.disabled]
5934
6109
  */
5935
- function CheckboxEntry(props) {
6110
+ function ToggleSwitchEntry(props) {
5936
6111
  const {
5937
6112
  element,
5938
6113
  id,
5939
6114
  description,
5940
6115
  label,
6116
+ switcherLabel,
6117
+ inline,
5941
6118
  getValue,
5942
6119
  setValue,
5943
- disabled,
5944
6120
  onFocus,
5945
6121
  onBlur,
5946
6122
  tooltip
5947
6123
  } = props;
5948
6124
  const value = getValue(element);
5949
- const error = useError(id);
5950
6125
  return jsxs("div", {
5951
- class: "bio-properties-panel-entry bio-properties-panel-checkbox-entry",
6126
+ class: "bio-properties-panel-entry bio-properties-panel-toggle-switch-entry",
5952
6127
  "data-entry-id": id,
5953
- children: [jsx(Checkbox, {
5954
- disabled: disabled,
6128
+ children: [jsx(ToggleSwitch, {
5955
6129
  id: id,
5956
6130
  label: label,
5957
- onChange: setValue,
6131
+ value: value,
6132
+ onInput: setValue,
5958
6133
  onFocus: onFocus,
5959
6134
  onBlur: onBlur,
5960
- value: value,
6135
+ switcherLabel: switcherLabel,
6136
+ inline: inline,
5961
6137
  tooltip: tooltip,
5962
6138
  element: element
5963
- }, element), error && jsx("div", {
5964
- class: "bio-properties-panel-error",
5965
- children: error
5966
6139
  }), jsx(Description$1, {
5967
6140
  forId: id,
5968
6141
  element: element,
@@ -5979,254 +6152,38 @@ function isEdited$8(node) {
5979
6152
  function prefixId$7(id) {
5980
6153
  return `bio-properties-panel-${id}`;
5981
6154
  }
5982
- const useBufferedFocus$1 = function (editor, ref) {
5983
- const [buffer, setBuffer] = useState(undefined);
5984
- ref.current = useMemo(() => ({
5985
- focus: offset => {
5986
- if (editor) {
5987
- editor.focus(offset);
5988
- } else {
5989
- if (typeof offset === 'undefined') {
5990
- offset = Infinity;
5991
- }
5992
- setBuffer(offset);
5993
- }
5994
- }
5995
- }), [editor]);
5996
- useEffect(() => {
5997
- if (typeof buffer !== 'undefined' && editor) {
5998
- editor.focus(buffer);
5999
- setBuffer(false);
6000
- }
6001
- }, [editor, buffer]);
6002
- };
6003
- const CodeEditor$1 = forwardRef((props, ref) => {
6155
+ function NumberField(props) {
6004
6156
  const {
6005
- onInput,
6157
+ debounce,
6006
6158
  disabled,
6007
- tooltipContainer,
6008
- enableGutters,
6009
- value,
6010
- onLint = () => {},
6011
- contentAttributes = {},
6012
- hostLanguage = null,
6013
- singleLine = false
6159
+ displayLabel = true,
6160
+ id,
6161
+ inputRef,
6162
+ label,
6163
+ max,
6164
+ min,
6165
+ onInput,
6166
+ step,
6167
+ value = '',
6168
+ onFocus,
6169
+ onBlur
6014
6170
  } = props;
6015
- const inputRef = useRef$1();
6016
- const [editor, setEditor] = useState();
6017
- const [localValue, setLocalValue] = useState(value || '');
6018
- useBufferedFocus$1(editor, ref);
6019
- const handleInput = useStaticCallback(newValue => {
6020
- onInput(newValue);
6021
- setLocalValue(newValue);
6022
- });
6023
- useEffect(() => {
6024
- let editor;
6025
- editor = new FeelersEditor({
6026
- container: inputRef.current,
6027
- onChange: handleInput,
6028
- value: localValue,
6029
- onLint,
6030
- contentAttributes,
6031
- tooltipContainer,
6032
- enableGutters,
6033
- hostLanguage,
6034
- singleLine
6171
+ const [localValue, setLocalValue] = useState(value);
6172
+ const handleInputCallback = useMemo(() => {
6173
+ return debounce(event => {
6174
+ const {
6175
+ validity,
6176
+ value
6177
+ } = event.target;
6178
+ if (validity.valid) {
6179
+ onInput(value ? parseFloat(value) : undefined);
6180
+ }
6035
6181
  });
6036
- setEditor(editor);
6037
- return () => {
6038
- onLint([]);
6039
- inputRef.current.innerHTML = '';
6040
- setEditor(null);
6041
- };
6042
- }, []);
6043
- useEffect(() => {
6044
- if (!editor) {
6045
- return;
6046
- }
6047
- if (value === localValue) {
6048
- return;
6049
- }
6050
- editor.setValue(value);
6051
- setLocalValue(value);
6052
- }, [value]);
6053
- const handleClick = () => {
6054
- ref.current.focus();
6055
- };
6056
- return jsx("div", {
6057
- name: props.name,
6058
- class: classnames('bio-properties-panel-feelers-editor bio-properties-panel-input', localValue ? 'edited' : null, disabled ? 'disabled' : null),
6059
- ref: inputRef,
6060
- onClick: handleClick
6061
- });
6062
- });
6063
- const useBufferedFocus = function (editor, ref) {
6064
- const [buffer, setBuffer] = useState(undefined);
6065
- ref.current = useMemo(() => ({
6066
- focus: offset => {
6067
- if (editor) {
6068
- editor.focus(offset);
6069
- } else {
6070
- if (typeof offset === 'undefined') {
6071
- offset = Infinity;
6072
- }
6073
- setBuffer(offset);
6074
- }
6075
- }
6076
- }), [editor]);
6077
- useEffect(() => {
6078
- if (typeof buffer !== 'undefined' && editor) {
6079
- editor.focus(buffer);
6080
- setBuffer(false);
6081
- }
6082
- }, [editor, buffer]);
6083
- };
6084
- const CodeEditor = forwardRef((props, ref) => {
6085
- const {
6086
- value,
6087
- onInput,
6088
- onFeelToggle,
6089
- onLint = () => {},
6090
- disabled,
6091
- tooltipContainer,
6092
- variables
6093
- } = props;
6094
- const inputRef = useRef$1();
6095
- const [editor, setEditor] = useState();
6096
- const [localValue, setLocalValue] = useState(value || '');
6097
- useBufferedFocus(editor, ref);
6098
- const handleInput = useStaticCallback(newValue => {
6099
- onInput(newValue);
6100
- setLocalValue(newValue);
6101
- });
6102
- useEffect(() => {
6103
- let editor;
6104
-
6105
- /* Trigger FEEL toggle when
6106
- *
6107
- * - `backspace` is pressed
6108
- * - AND the cursor is at the beginning of the input
6109
- */
6110
- const onKeyDown = e => {
6111
- if (e.key !== 'Backspace' || !editor) {
6112
- return;
6113
- }
6114
- const selection = editor.getSelection();
6115
- const range = selection.ranges[selection.mainIndex];
6116
- if (range.from === 0 && range.to === 0) {
6117
- onFeelToggle();
6118
- }
6119
- };
6120
- editor = new FeelEditor({
6121
- container: inputRef.current,
6122
- onChange: handleInput,
6123
- onKeyDown: onKeyDown,
6124
- onLint: onLint,
6125
- tooltipContainer: tooltipContainer,
6126
- value: localValue,
6127
- variables: variables
6128
- });
6129
- setEditor(editor);
6130
- return () => {
6131
- onLint([]);
6132
- inputRef.current.innerHTML = '';
6133
- setEditor(null);
6134
- };
6135
- }, []);
6136
- useEffect(() => {
6137
- if (!editor) {
6138
- return;
6139
- }
6140
- if (value === localValue) {
6141
- return;
6142
- }
6143
- editor.setValue(value);
6144
- setLocalValue(value);
6145
- }, [value]);
6146
- useEffect(() => {
6147
- if (!editor) {
6148
- return;
6149
- }
6150
- editor.setVariables(variables);
6151
- }, [variables]);
6152
- const handleClick = () => {
6153
- ref.current.focus();
6154
- };
6155
- return jsx("div", {
6156
- class: classnames('bio-properties-panel-feel-editor-container', disabled ? 'disabled' : null),
6157
- children: jsx("div", {
6158
- name: props.name,
6159
- class: classnames('bio-properties-panel-input', localValue ? 'edited' : null),
6160
- ref: inputRef,
6161
- onClick: handleClick
6162
- })
6163
- });
6164
- });
6165
- function FeelIndicator(props) {
6166
- const {
6167
- active
6168
- } = props;
6169
- if (!active) {
6170
- return null;
6171
- }
6172
- return jsx("span", {
6173
- class: "bio-properties-panel-feel-indicator",
6174
- children: "="
6175
- });
6176
- }
6177
- const noop$2 = () => {};
6178
-
6179
- /**
6180
- * @param {Object} props
6181
- * @param {Object} props.label
6182
- * @param {String} props.feel
6183
- */
6184
- function FeelIcon(props) {
6185
- const {
6186
- feel = false,
6187
- active,
6188
- disabled = false,
6189
- onClick = noop$2
6190
- } = props;
6191
- const feelRequiredLabel = 'FEEL expression is mandatory';
6192
- const feelOptionalLabel = `Click to ${active ? 'remove' : 'set a'} dynamic value with FEEL expression`;
6193
- const handleClick = e => {
6194
- onClick(e);
6195
-
6196
- // when pointer event was created from keyboard, keep focus on button
6197
- if (!e.pointerType) {
6198
- e.stopPropagation();
6199
- }
6200
- };
6201
- return jsx("button", {
6202
- class: classnames('bio-properties-panel-feel-icon', active ? 'active' : null, feel === 'required' ? 'required' : 'optional'),
6203
- onClick: handleClick,
6204
- disabled: feel === 'required' || disabled,
6205
- title: feel === 'required' ? feelRequiredLabel : feelOptionalLabel,
6206
- children: jsx(FeelIcon$1, {})
6207
- });
6208
- }
6209
- function ToggleSwitch(props) {
6210
- const {
6211
- id,
6212
- label,
6213
- onInput,
6214
- value,
6215
- switcherLabel,
6216
- inline,
6217
- onFocus,
6218
- onBlur,
6219
- inputRef,
6220
- tooltip
6221
- } = props;
6222
- const [localValue, setLocalValue] = useState(value);
6223
- const handleInputCallback = async () => {
6224
- onInput(!value);
6225
- };
6226
- const handleInput = e => {
6227
- handleInputCallback();
6228
- setLocalValue(e.target.value);
6229
- };
6182
+ }, [onInput, debounce]);
6183
+ const handleInput = e => {
6184
+ handleInputCallback(e);
6185
+ setLocalValue(e.target.value);
6186
+ };
6230
6187
  useEffect(() => {
6231
6188
  if (value === localValue) {
6232
6189
  return;
@@ -6234,199 +6191,64 @@ function ToggleSwitch(props) {
6234
6191
  setLocalValue(value);
6235
6192
  }, [value]);
6236
6193
  return jsxs("div", {
6237
- class: classnames('bio-properties-panel-toggle-switch', {
6238
- inline
6239
- }),
6240
- children: [jsx("label", {
6241
- class: "bio-properties-panel-label",
6194
+ class: "bio-properties-panel-numberfield",
6195
+ children: [displayLabel && jsx("label", {
6242
6196
  for: prefixId$6(id),
6243
- children: jsx(TooltipWrapper, {
6244
- value: tooltip,
6245
- forId: id,
6246
- element: props.element,
6247
- children: label
6248
- })
6249
- }), jsxs("div", {
6250
- class: "bio-properties-panel-field-wrapper",
6251
- children: [jsxs("label", {
6252
- class: "bio-properties-panel-toggle-switch__switcher",
6253
- children: [jsx("input", {
6254
- ref: inputRef,
6255
- id: prefixId$6(id),
6256
- class: "bio-properties-panel-input",
6257
- type: "checkbox",
6258
- onFocus: onFocus,
6259
- onBlur: onBlur,
6260
- name: id,
6261
- onInput: handleInput,
6262
- checked: !!localValue
6263
- }), jsx("span", {
6264
- class: "bio-properties-panel-toggle-switch__slider"
6265
- })]
6266
- }), switcherLabel && jsx("p", {
6267
- class: "bio-properties-panel-toggle-switch__label",
6268
- children: switcherLabel
6269
- })]
6197
+ class: "bio-properties-panel-label",
6198
+ children: label
6199
+ }), jsx("input", {
6200
+ id: prefixId$6(id),
6201
+ ref: inputRef,
6202
+ type: "number",
6203
+ name: id,
6204
+ spellCheck: "false",
6205
+ autoComplete: "off",
6206
+ disabled: disabled,
6207
+ class: "bio-properties-panel-input",
6208
+ max: max,
6209
+ min: min,
6210
+ onInput: handleInput,
6211
+ onFocus: onFocus,
6212
+ onBlur: onBlur,
6213
+ step: step,
6214
+ value: localValue
6270
6215
  })]
6271
6216
  });
6272
6217
  }
6273
6218
 
6274
6219
  /**
6275
6220
  * @param {Object} props
6221
+ * @param {Boolean} props.debounce
6222
+ * @param {String} props.description
6223
+ * @param {Boolean} props.disabled
6276
6224
  * @param {Object} props.element
6225
+ * @param {Function} props.getValue
6277
6226
  * @param {String} props.id
6278
- * @param {String} props.description
6279
6227
  * @param {String} props.label
6280
- * @param {String} props.switcherLabel
6281
- * @param {Boolean} props.inline
6282
- * @param {Function} props.getValue
6228
+ * @param {String} props.max
6229
+ * @param {String} props.min
6283
6230
  * @param {Function} props.setValue
6284
6231
  * @param {Function} props.onFocus
6285
6232
  * @param {Function} props.onBlur
6286
- * @param {string|import('preact').Component} props.tooltip
6233
+ * @param {String} props.step
6234
+ * @param {Function} props.validate
6287
6235
  */
6288
- function ToggleSwitchEntry(props) {
6236
+ function NumberFieldEntry(props) {
6289
6237
  const {
6238
+ debounce,
6239
+ description,
6240
+ disabled,
6290
6241
  element,
6242
+ getValue,
6291
6243
  id,
6292
- description,
6293
6244
  label,
6294
- switcherLabel,
6295
- inline,
6296
- getValue,
6245
+ max,
6246
+ min,
6297
6247
  setValue,
6248
+ step,
6298
6249
  onFocus,
6299
6250
  onBlur,
6300
- tooltip
6301
- } = props;
6302
- const value = getValue(element);
6303
- return jsxs("div", {
6304
- class: "bio-properties-panel-entry bio-properties-panel-toggle-switch-entry",
6305
- "data-entry-id": id,
6306
- children: [jsx(ToggleSwitch, {
6307
- id: id,
6308
- label: label,
6309
- value: value,
6310
- onInput: setValue,
6311
- onFocus: onFocus,
6312
- onBlur: onBlur,
6313
- switcherLabel: switcherLabel,
6314
- inline: inline,
6315
- tooltip: tooltip,
6316
- element: element
6317
- }), jsx(Description$1, {
6318
- forId: id,
6319
- element: element,
6320
- value: description
6321
- })]
6322
- });
6323
- }
6324
- function isEdited$7(node) {
6325
- return node && !!node.checked;
6326
- }
6327
-
6328
- // helpers /////////////////
6329
-
6330
- function prefixId$6(id) {
6331
- return `bio-properties-panel-${id}`;
6332
- }
6333
- function NumberField(props) {
6334
- const {
6335
- debounce,
6336
- disabled,
6337
- displayLabel = true,
6338
- id,
6339
- inputRef,
6340
- label,
6341
- max,
6342
- min,
6343
- onInput,
6344
- step,
6345
- value = '',
6346
- onFocus,
6347
- onBlur
6348
- } = props;
6349
- const [localValue, setLocalValue] = useState(value);
6350
- const handleInputCallback = useMemo(() => {
6351
- return debounce(event => {
6352
- const {
6353
- validity,
6354
- value
6355
- } = event.target;
6356
- if (validity.valid) {
6357
- onInput(value ? parseFloat(value) : undefined);
6358
- }
6359
- });
6360
- }, [onInput, debounce]);
6361
- const handleInput = e => {
6362
- handleInputCallback(e);
6363
- setLocalValue(e.target.value);
6364
- };
6365
- useEffect(() => {
6366
- if (value === localValue) {
6367
- return;
6368
- }
6369
- setLocalValue(value);
6370
- }, [value]);
6371
- return jsxs("div", {
6372
- class: "bio-properties-panel-numberfield",
6373
- children: [displayLabel && jsx("label", {
6374
- for: prefixId$5(id),
6375
- class: "bio-properties-panel-label",
6376
- children: label
6377
- }), jsx("input", {
6378
- id: prefixId$5(id),
6379
- ref: inputRef,
6380
- type: "number",
6381
- name: id,
6382
- spellCheck: "false",
6383
- autoComplete: "off",
6384
- disabled: disabled,
6385
- class: "bio-properties-panel-input",
6386
- max: max,
6387
- min: min,
6388
- onInput: handleInput,
6389
- onFocus: onFocus,
6390
- onBlur: onBlur,
6391
- step: step,
6392
- value: localValue
6393
- })]
6394
- });
6395
- }
6396
-
6397
- /**
6398
- * @param {Object} props
6399
- * @param {Boolean} props.debounce
6400
- * @param {String} props.description
6401
- * @param {Boolean} props.disabled
6402
- * @param {Object} props.element
6403
- * @param {Function} props.getValue
6404
- * @param {String} props.id
6405
- * @param {String} props.label
6406
- * @param {String} props.max
6407
- * @param {String} props.min
6408
- * @param {Function} props.setValue
6409
- * @param {Function} props.onFocus
6410
- * @param {Function} props.onBlur
6411
- * @param {String} props.step
6412
- * @param {Function} props.validate
6413
- */
6414
- function NumberFieldEntry(props) {
6415
- const {
6416
- debounce,
6417
- description,
6418
- disabled,
6419
- element,
6420
- getValue,
6421
- id,
6422
- label,
6423
- max,
6424
- min,
6425
- setValue,
6426
- step,
6427
- onFocus,
6428
- onBlur,
6429
- validate
6251
+ validate
6430
6252
  } = props;
6431
6253
  const globalError = useError(id);
6432
6254
  const [localError, setLocalError] = useState(null);
@@ -6471,27 +6293,30 @@ function NumberFieldEntry(props) {
6471
6293
  })]
6472
6294
  });
6473
6295
  }
6474
- function isEdited$6(node) {
6296
+ function isEdited$7(node) {
6475
6297
  return node && !!node.value;
6476
6298
  }
6477
6299
 
6478
6300
  // helpers /////////////////
6479
6301
 
6480
- function prefixId$5(id) {
6302
+ function prefixId$6(id) {
6481
6303
  return `bio-properties-panel-${id}`;
6482
6304
  }
6483
- const noop$1 = () => {};
6305
+ const noop$2 = () => {};
6484
6306
  function FeelTextfield(props) {
6485
6307
  const {
6486
6308
  debounce,
6487
6309
  id,
6310
+ element,
6488
6311
  label,
6312
+ hostLanguage,
6489
6313
  onInput,
6490
6314
  onError,
6491
6315
  feel,
6492
6316
  value = '',
6493
6317
  disabled = false,
6494
6318
  variables,
6319
+ singleLine,
6495
6320
  tooltipContainer,
6496
6321
  OptionalComponent = OptionalFeelInput,
6497
6322
  tooltip
@@ -6502,6 +6327,11 @@ function FeelTextfield(props) {
6502
6327
  const feelActive = isString(localValue) && localValue.startsWith('=') || feel === 'required';
6503
6328
  const feelOnlyValue = isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
6504
6329
  const [focus, _setFocus] = useState(undefined);
6330
+ const {
6331
+ open: openPopup,
6332
+ source: popupSource
6333
+ } = useContext(FeelPopupContext);
6334
+ const popuOpen = popupSource === id;
6505
6335
  const setFocus = (offset = 0) => {
6506
6336
  const hasFocus = containerRef.current.contains(document.activeElement);
6507
6337
 
@@ -6554,6 +6384,21 @@ function FeelTextfield(props) {
6554
6384
  const message = `${error.source}: ${error.message}`;
6555
6385
  onError(message);
6556
6386
  });
6387
+ const handlePopupOpen = (type = 'feel') => {
6388
+ const popupOptions = {
6389
+ id,
6390
+ hostLanguage,
6391
+ onInput: handleLocalInput,
6392
+ position: calculatePopupPosition(containerRef.current),
6393
+ singleLine,
6394
+ title: getPopupTitle(element, label),
6395
+ tooltipContainer,
6396
+ type,
6397
+ value: feelOnlyValue,
6398
+ variables
6399
+ };
6400
+ openPopup(id, popupOptions, editorRef.current);
6401
+ };
6557
6402
  useEffect(() => {
6558
6403
  if (typeof focus !== 'undefined') {
6559
6404
  editorRef.current.focus(focus);
@@ -6582,7 +6427,7 @@ function FeelTextfield(props) {
6582
6427
  event.clipboardData.setData('application/FEEL', event.clipboardData.getData('text'));
6583
6428
  };
6584
6429
  const pasteHandler = event => {
6585
- if (feelActive) {
6430
+ if (feelActive || popuOpen) {
6586
6431
  return;
6587
6432
  }
6588
6433
  const data = event.clipboardData.getData('application/FEEL');
@@ -6607,7 +6452,7 @@ function FeelTextfield(props) {
6607
6452
  'feel-active': feelActive
6608
6453
  }),
6609
6454
  children: [jsxs("label", {
6610
- for: prefixId$4(id),
6455
+ for: prefixId$5(id),
6611
6456
  class: "bio-properties-panel-label",
6612
6457
  onClick: () => setFocus(),
6613
6458
  children: [jsx(TooltipWrapper, {
@@ -6629,28 +6474,33 @@ function FeelTextfield(props) {
6629
6474
  disabled: feel !== 'optional' || disabled,
6630
6475
  onClick: handleFeelToggle
6631
6476
  }), feelActive ? jsx(CodeEditor, {
6632
- id: prefixId$4(id),
6477
+ id: prefixId$5(id),
6633
6478
  name: id,
6634
6479
  onInput: handleLocalInput,
6635
6480
  disabled: disabled,
6481
+ popupOpen: popuOpen,
6636
6482
  onFeelToggle: () => {
6637
6483
  handleFeelToggle();
6638
6484
  setFocus(true);
6639
6485
  },
6640
6486
  onLint: handleLint,
6487
+ onPopupOpen: handlePopupOpen,
6641
6488
  value: feelOnlyValue,
6642
6489
  variables: variables,
6643
6490
  ref: editorRef,
6644
6491
  tooltipContainer: tooltipContainer
6645
6492
  }) : jsx(OptionalComponent, {
6646
6493
  ...props,
6494
+ popupOpen: popuOpen,
6647
6495
  onInput: handleLocalInput,
6648
6496
  contentAttributes: {
6649
- 'id': prefixId$4(id),
6497
+ 'id': prefixId$5(id),
6650
6498
  'aria-label': label
6651
6499
  },
6652
6500
  value: localValue,
6653
- ref: editorRef
6501
+ ref: editorRef,
6502
+ onPopupOpen: handlePopupOpen,
6503
+ containerRef: containerRef
6654
6504
  })]
6655
6505
  })]
6656
6506
  });
@@ -6684,7 +6534,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
6684
6534
  }
6685
6535
  };
6686
6536
  return jsx("input", {
6687
- id: prefixId$4(id),
6537
+ id: prefixId$5(id),
6688
6538
  type: "text",
6689
6539
  ref: inputRef,
6690
6540
  name: id,
@@ -6711,309 +6561,995 @@ const OptionalFeelNumberField = forwardRef((props, ref) => {
6711
6561
  onFocus,
6712
6562
  onBlur
6713
6563
  } = props;
6714
- const inputRef = useRef$1();
6564
+ const inputRef = useRef$1();
6565
+
6566
+ // To be consistent with the FEEL editor, set focus at start of input
6567
+ // this ensures clean editing experience when switching with the keyboard
6568
+ ref.current = {
6569
+ focus: position => {
6570
+ const input = inputRef.current;
6571
+ if (!input) {
6572
+ return;
6573
+ }
6574
+ input.focus();
6575
+ if (typeof position === 'number' && position !== Infinity) {
6576
+ if (position > value.length) {
6577
+ position = value.length;
6578
+ }
6579
+ input.setSelectionRange(position, position);
6580
+ }
6581
+ }
6582
+ };
6583
+ return jsx(NumberField, {
6584
+ id: id,
6585
+ debounce: debounce,
6586
+ disabled: disabled,
6587
+ displayLabel: false,
6588
+ inputRef: inputRef,
6589
+ max: max,
6590
+ min: min,
6591
+ onInput: onInput,
6592
+ step: step,
6593
+ value: value,
6594
+ onFocus: onFocus,
6595
+ onBlur: onBlur
6596
+ });
6597
+ });
6598
+ forwardRef((props, ref) => {
6599
+ const {
6600
+ id,
6601
+ disabled,
6602
+ onInput,
6603
+ value,
6604
+ onFocus,
6605
+ onBlur
6606
+ } = props;
6607
+ const inputRef = useRef$1();
6608
+
6609
+ // To be consistent with the FEEL editor, set focus at start of input
6610
+ // this ensures clean editing experience when switching with the keyboard
6611
+ ref.current = {
6612
+ focus: () => {
6613
+ const input = inputRef.current;
6614
+ if (!input) {
6615
+ return;
6616
+ }
6617
+ input.focus();
6618
+ input.setSelectionRange(0, 0);
6619
+ }
6620
+ };
6621
+ return jsx("textarea", {
6622
+ id: prefixId$5(id),
6623
+ type: "text",
6624
+ ref: inputRef,
6625
+ name: id,
6626
+ spellCheck: "false",
6627
+ autoComplete: "off",
6628
+ disabled: disabled,
6629
+ class: "bio-properties-panel-input",
6630
+ onInput: e => onInput(e.target.value),
6631
+ onFocus: onFocus,
6632
+ onBlur: onBlur,
6633
+ value: value || '',
6634
+ "data-gramm": "false"
6635
+ });
6636
+ });
6637
+ const OptionalFeelToggleSwitch = forwardRef((props, ref) => {
6638
+ const {
6639
+ id,
6640
+ onInput,
6641
+ value,
6642
+ onFocus,
6643
+ onBlur,
6644
+ switcherLabel
6645
+ } = props;
6646
+ const inputRef = useRef$1();
6647
+
6648
+ // To be consistent with the FEEL editor, set focus at start of input
6649
+ // this ensures clean editing experience when switching with the keyboard
6650
+ ref.current = {
6651
+ focus: () => {
6652
+ const input = inputRef.current;
6653
+ if (!input) {
6654
+ return;
6655
+ }
6656
+ input.focus();
6657
+ }
6658
+ };
6659
+ return jsx(ToggleSwitch, {
6660
+ id: id,
6661
+ value: value,
6662
+ inputRef: inputRef,
6663
+ onInput: onInput,
6664
+ onFocus: onFocus,
6665
+ onBlur: onBlur,
6666
+ switcherLabel: switcherLabel
6667
+ });
6668
+ });
6669
+ forwardRef((props, ref) => {
6670
+ const {
6671
+ id,
6672
+ disabled,
6673
+ onInput,
6674
+ value,
6675
+ onFocus,
6676
+ onBlur
6677
+ } = props;
6678
+ const inputRef = useRef$1();
6679
+ const handleChange = ({
6680
+ target
6681
+ }) => {
6682
+ onInput(target.checked);
6683
+ };
6684
+
6685
+ // To be consistent with the FEEL editor, set focus at start of input
6686
+ // this ensures clean editing experience when switching with the keyboard
6687
+ ref.current = {
6688
+ focus: () => {
6689
+ const input = inputRef.current;
6690
+ if (!input) {
6691
+ return;
6692
+ }
6693
+ input.focus();
6694
+ }
6695
+ };
6696
+ return jsx("input", {
6697
+ ref: inputRef,
6698
+ id: prefixId$5(id),
6699
+ name: id,
6700
+ onFocus: onFocus,
6701
+ onBlur: onBlur,
6702
+ type: "checkbox",
6703
+ class: "bio-properties-panel-input",
6704
+ onChange: handleChange,
6705
+ checked: value,
6706
+ disabled: disabled
6707
+ });
6708
+ });
6709
+
6710
+ /**
6711
+ * @param {Object} props
6712
+ * @param {Object} props.element
6713
+ * @param {String} props.id
6714
+ * @param {String} props.description
6715
+ * @param {Boolean} props.debounce
6716
+ * @param {Boolean} props.disabled
6717
+ * @param {Boolean} props.feel
6718
+ * @param {String} props.label
6719
+ * @param {Function} props.getValue
6720
+ * @param {Function} props.setValue
6721
+ * @param {Function} props.tooltipContainer
6722
+ * @param {Function} props.validate
6723
+ * @param {Function} props.show
6724
+ * @param {Function} props.example
6725
+ * @param {Function} props.variables
6726
+ * @param {Function} props.onFocus
6727
+ * @param {Function} props.onBlur
6728
+ * @param {string|import('preact').Component} props.tooltip
6729
+ */
6730
+ function FeelEntry(props) {
6731
+ const {
6732
+ element,
6733
+ id,
6734
+ description,
6735
+ debounce,
6736
+ disabled,
6737
+ feel,
6738
+ label,
6739
+ getValue,
6740
+ setValue,
6741
+ tooltipContainer,
6742
+ hostLanguage,
6743
+ singleLine,
6744
+ validate,
6745
+ show = noop$2,
6746
+ example,
6747
+ variables,
6748
+ onFocus,
6749
+ onBlur,
6750
+ tooltip
6751
+ } = props;
6752
+ const [validationError, setValidationError] = useState(null);
6753
+ const [localError, setLocalError] = useState(null);
6754
+ let value = getValue(element);
6755
+ useEffect(() => {
6756
+ if (isFunction(validate)) {
6757
+ const newValidationError = validate(value) || null;
6758
+ setValidationError(newValidationError);
6759
+ }
6760
+ }, [value]);
6761
+ const onInput = useStaticCallback(newValue => {
6762
+ let newValidationError = null;
6763
+ if (isFunction(validate)) {
6764
+ newValidationError = validate(newValue) || null;
6765
+ }
6766
+
6767
+ // don't create multiple commandStack entries for the same value
6768
+ if (newValue !== value) {
6769
+ setValue(newValue, newValidationError);
6770
+ }
6771
+ setValidationError(newValidationError);
6772
+ });
6773
+ const onError = useCallback(err => {
6774
+ setLocalError(err);
6775
+ }, []);
6776
+ const temporaryError = useError(id);
6777
+ const error = localError || temporaryError || validationError;
6778
+ return jsxs("div", {
6779
+ class: classnames(props.class, 'bio-properties-panel-entry', error ? 'has-error' : ''),
6780
+ "data-entry-id": id,
6781
+ children: [createElement(FeelTextfield, {
6782
+ ...props,
6783
+ debounce: debounce,
6784
+ disabled: disabled,
6785
+ feel: feel,
6786
+ id: id,
6787
+ key: element,
6788
+ label: label,
6789
+ onInput: onInput,
6790
+ onError: onError,
6791
+ onFocus: onFocus,
6792
+ onBlur: onBlur,
6793
+ example: example,
6794
+ hostLanguage: hostLanguage,
6795
+ singleLine: singleLine,
6796
+ show: show,
6797
+ value: value,
6798
+ variables: variables,
6799
+ tooltipContainer: tooltipContainer,
6800
+ OptionalComponent: props.OptionalComponent,
6801
+ tooltip: tooltip
6802
+ }), error && jsx("div", {
6803
+ class: "bio-properties-panel-error",
6804
+ children: error
6805
+ }), jsx(Description$1, {
6806
+ forId: id,
6807
+ element: element,
6808
+ value: description
6809
+ })]
6810
+ });
6811
+ }
6812
+
6813
+ /**
6814
+ * @param {Object} props
6815
+ * @param {Object} props.element
6816
+ * @param {String} props.id
6817
+ * @param {String} props.description
6818
+ * @param {Boolean} props.debounce
6819
+ * @param {Boolean} props.disabled
6820
+ * @param {String} props.max
6821
+ * @param {String} props.min
6822
+ * @param {String} props.step
6823
+ * @param {Boolean} props.feel
6824
+ * @param {String} props.label
6825
+ * @param {Function} props.getValue
6826
+ * @param {Function} props.setValue
6827
+ * @param {Function} props.tooltipContainer
6828
+ * @param {Function} props.validate
6829
+ * @param {Function} props.show
6830
+ * @param {Function} props.example
6831
+ * @param {Function} props.variables
6832
+ * @param {Function} props.onFocus
6833
+ * @param {Function} props.onBlur
6834
+ */
6835
+ function FeelNumberEntry(props) {
6836
+ return jsx(FeelEntry, {
6837
+ class: "bio-properties-panel-feel-number",
6838
+ OptionalComponent: OptionalFeelNumberField,
6839
+ ...props
6840
+ });
6841
+ }
6842
+
6843
+ /**
6844
+ * @param {Object} props
6845
+ * @param {Object} props.element
6846
+ * @param {String} props.id
6847
+ * @param {String} props.description
6848
+ * @param {Boolean} props.debounce
6849
+ * @param {Boolean} props.disabled
6850
+ * @param {Boolean} props.feel
6851
+ * @param {String} props.label
6852
+ * @param {Function} props.getValue
6853
+ * @param {Function} props.setValue
6854
+ * @param {Function} props.tooltipContainer
6855
+ * @param {Function} props.validate
6856
+ * @param {Function} props.show
6857
+ * @param {Function} props.example
6858
+ * @param {Function} props.variables
6859
+ * @param {Function} props.onFocus
6860
+ * @param {Function} props.onBlur
6861
+ */
6862
+ function FeelToggleSwitchEntry(props) {
6863
+ return jsx(FeelEntry, {
6864
+ class: "bio-properties-panel-feel-toggle-switch",
6865
+ OptionalComponent: OptionalFeelToggleSwitch,
6866
+ ...props
6867
+ });
6868
+ }
6869
+
6870
+ /**
6871
+ * @param {Object} props
6872
+ * @param {Object} props.element
6873
+ * @param {String} props.id
6874
+ * @param {String} props.description
6875
+ * @param {String} props.hostLanguage
6876
+ * @param {Boolean} props.singleLine
6877
+ * @param {Boolean} props.debounce
6878
+ * @param {Boolean} props.disabled
6879
+ * @param {Boolean} props.feel
6880
+ * @param {String} props.label
6881
+ * @param {Function} props.getValue
6882
+ * @param {Function} props.setValue
6883
+ * @param {Function} props.tooltipContainer
6884
+ * @param {Function} props.validate
6885
+ * @param {Function} props.show
6886
+ * @param {Function} props.example
6887
+ * @param {Function} props.variables
6888
+ * @param {Function} props.onFocus
6889
+ * @param {Function} props.onBlur
6890
+ */
6891
+ function FeelTemplatingEntry(props) {
6892
+ return jsx(FeelEntry, {
6893
+ class: "bio-properties-panel-feel-templating",
6894
+ OptionalComponent: CodeEditor$1,
6895
+ ...props
6896
+ });
6897
+ }
6898
+ function isEdited$6(node) {
6899
+ if (!node) {
6900
+ return false;
6901
+ }
6902
+ if (node.type === 'checkbox') {
6903
+ return !!node.checked || node.classList.contains('edited');
6904
+ }
6905
+ return !!node.value || node.classList.contains('edited');
6906
+ }
6907
+
6908
+ // helpers /////////////////
6909
+
6910
+ function prefixId$5(id) {
6911
+ return `bio-properties-panel-${id}`;
6912
+ }
6913
+ function calculatePopupPosition(element) {
6914
+ const {
6915
+ top,
6916
+ left
6917
+ } = element.getBoundingClientRect();
6918
+ return {
6919
+ left: left - FEEL_POPUP_WIDTH - 20,
6920
+ top: top
6921
+ };
6922
+ }
6923
+
6924
+ // todo(pinussilvestrus): make this configurable in the future
6925
+ function getPopupTitle(element, label) {
6926
+ let popupTitle;
6927
+ if (element && element.type) {
6928
+ popupTitle = `${element.type} / `;
6929
+ }
6930
+ return `${popupTitle}${label}`;
6931
+ }
6932
+ const DEFAULT_LAYOUT = {};
6933
+ const DEFAULT_DESCRIPTION = {};
6934
+ const DEFAULT_TOOLTIP = {};
6935
+
6936
+ /**
6937
+ * @typedef { {
6938
+ * component: import('preact').Component,
6939
+ * id: String,
6940
+ * isEdited?: Function
6941
+ * } } EntryDefinition
6942
+ *
6943
+ * @typedef { {
6944
+ * autoFocusEntry: String,
6945
+ * autoOpen?: Boolean,
6946
+ * entries: Array<EntryDefinition>,
6947
+ * id: String,
6948
+ * label: String,
6949
+ * remove: (event: MouseEvent) => void
6950
+ * } } ListItemDefinition
6951
+ *
6952
+ * @typedef { {
6953
+ * add: (event: MouseEvent) => void,
6954
+ * component: import('preact').Component,
6955
+ * element: Object,
6956
+ * id: String,
6957
+ * items: Array<ListItemDefinition>,
6958
+ * label: String,
6959
+ * shouldSort?: Boolean,
6960
+ * shouldOpen?: Boolean
6961
+ * } } ListGroupDefinition
6962
+ *
6963
+ * @typedef { {
6964
+ * component?: import('preact').Component,
6965
+ * entries: Array<EntryDefinition>,
6966
+ * id: String,
6967
+ * label: String,
6968
+ * shouldOpen?: Boolean
6969
+ * } } GroupDefinition
6970
+ *
6971
+ * @typedef { {
6972
+ * [id: String]: GetDescriptionFunction
6973
+ * } } DescriptionConfig
6974
+ *
6975
+ * @typedef { {
6976
+ * [id: String]: GetTooltipFunction
6977
+ * } } TooltipConfig
6978
+ *
6979
+ * @callback { {
6980
+ * @param {string} id
6981
+ * @param {Object} element
6982
+ * @returns {string}
6983
+ * } } GetDescriptionFunction
6984
+ *
6985
+ * @callback { {
6986
+ * @param {string} id
6987
+ * @param {Object} element
6988
+ * @returns {string}
6989
+ * } } GetTooltipFunction
6990
+ *
6991
+ * @typedef { {
6992
+ * getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
6993
+ * getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
6994
+ * } } PlaceholderProvider
6995
+ *
6996
+ */
6997
+
6998
+ /**
6999
+ * A basic properties panel component. Describes *how* content will be rendered, accepts
7000
+ * data from implementor to describe *what* will be rendered.
7001
+ *
7002
+ * @param {Object} props
7003
+ * @param {Object|Array} props.element
7004
+ * @param {import('./components/Header').HeaderProvider} props.headerProvider
7005
+ * @param {PlaceholderProvider} [props.placeholderProvider]
7006
+ * @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
7007
+ * @param {Object} [props.layoutConfig]
7008
+ * @param {Function} [props.layoutChanged]
7009
+ * @param {DescriptionConfig} [props.descriptionConfig]
7010
+ * @param {Function} [props.descriptionLoaded]
7011
+ * @param {TooltipConfig} [props.tooltipConfig]
7012
+ * @param {Function} [props.tooltipLoaded]
7013
+ * @param {Object} [props.eventBus]
7014
+ */
7015
+ function PropertiesPanel(props) {
7016
+ const {
7017
+ element,
7018
+ headerProvider,
7019
+ placeholderProvider,
7020
+ groups,
7021
+ layoutConfig,
7022
+ layoutChanged,
7023
+ descriptionConfig,
7024
+ descriptionLoaded,
7025
+ tooltipConfig,
7026
+ tooltipLoaded,
7027
+ eventBus
7028
+ } = props;
7029
+
7030
+ // set-up layout context
7031
+ const [layout, setLayout] = useState(createLayout(layoutConfig));
7032
+
7033
+ // react to external changes in the layout config
7034
+ useUpdateLayoutEffect(() => {
7035
+ const newLayout = createLayout(layoutConfig);
7036
+ setLayout(newLayout);
7037
+ }, [layoutConfig]);
7038
+ useEffect(() => {
7039
+ if (typeof layoutChanged === 'function') {
7040
+ layoutChanged(layout);
7041
+ }
7042
+ }, [layout, layoutChanged]);
7043
+ const getLayoutForKey = (key, defaultValue) => {
7044
+ return get(layout, key, defaultValue);
7045
+ };
7046
+ const setLayoutForKey = (key, config) => {
7047
+ const newLayout = assign({}, layout);
7048
+ set$1(newLayout, key, config);
7049
+ setLayout(newLayout);
7050
+ };
7051
+ const layoutContext = {
7052
+ layout,
7053
+ setLayout,
7054
+ getLayoutForKey,
7055
+ setLayoutForKey
7056
+ };
7057
+
7058
+ // set-up description context
7059
+ const description = useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
7060
+ useEffect(() => {
7061
+ if (typeof descriptionLoaded === 'function') {
7062
+ descriptionLoaded(description);
7063
+ }
7064
+ }, [description, descriptionLoaded]);
7065
+ const getDescriptionForId = (id, element) => {
7066
+ return description[id] && description[id](element);
7067
+ };
7068
+ const descriptionContext = {
7069
+ description,
7070
+ getDescriptionForId
7071
+ };
7072
+
7073
+ // set-up tooltip context
7074
+ const tooltip = useMemo(() => createTooltipContext(tooltipConfig), [tooltipConfig]);
7075
+ useEffect(() => {
7076
+ if (typeof tooltipLoaded === 'function') {
7077
+ tooltipLoaded(tooltip);
7078
+ }
7079
+ }, [tooltip, tooltipLoaded]);
7080
+ const getTooltipForId = (id, element) => {
7081
+ return tooltip[id] && tooltip[id](element);
7082
+ };
7083
+ const tooltipContext = {
7084
+ tooltip,
7085
+ getTooltipForId
7086
+ };
7087
+ const [errors, setErrors] = useState({});
7088
+ const onSetErrors = ({
7089
+ errors
7090
+ }) => setErrors(errors);
7091
+ useEvent('propertiesPanel.setErrors', onSetErrors, eventBus);
7092
+ const errorsContext = {
7093
+ errors
7094
+ };
7095
+ const eventContext = {
7096
+ eventBus
7097
+ };
7098
+ const propertiesPanelContext = {
7099
+ element
7100
+ };
7101
+
7102
+ // empty state
7103
+ if (placeholderProvider && !element) {
7104
+ return jsx(Placeholder, {
7105
+ ...placeholderProvider.getEmpty()
7106
+ });
7107
+ }
7108
+
7109
+ // multiple state
7110
+ if (placeholderProvider && isArray(element)) {
7111
+ return jsx(Placeholder, {
7112
+ ...placeholderProvider.getMultiple()
7113
+ });
7114
+ }
7115
+ return jsx(LayoutContext.Provider, {
7116
+ value: propertiesPanelContext,
7117
+ children: jsx(ErrorsContext.Provider, {
7118
+ value: errorsContext,
7119
+ children: jsx(DescriptionContext.Provider, {
7120
+ value: descriptionContext,
7121
+ children: jsx(TooltipContext.Provider, {
7122
+ value: tooltipContext,
7123
+ children: jsx(LayoutContext.Provider, {
7124
+ value: layoutContext,
7125
+ children: jsx(EventContext.Provider, {
7126
+ value: eventContext,
7127
+ children: jsx(FEELPopupRoot, {
7128
+ element: element,
7129
+ children: jsxs("div", {
7130
+ class: "bio-properties-panel",
7131
+ children: [jsx(Header, {
7132
+ element: element,
7133
+ headerProvider: headerProvider
7134
+ }), jsx("div", {
7135
+ class: "bio-properties-panel-scroll-container",
7136
+ children: groups.map(group => {
7137
+ const {
7138
+ component: Component = Group,
7139
+ id
7140
+ } = group;
7141
+ return createElement(Component, {
7142
+ ...group,
7143
+ key: id,
7144
+ element: element
7145
+ });
7146
+ })
7147
+ })]
7148
+ })
7149
+ })
7150
+ })
7151
+ })
7152
+ })
7153
+ })
7154
+ })
7155
+ });
7156
+ }
7157
+
7158
+ // helpers //////////////////
7159
+
7160
+ function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
7161
+ return {
7162
+ ...defaults,
7163
+ ...overrides
7164
+ };
7165
+ }
7166
+ function createDescriptionContext(overrides = {}) {
7167
+ return {
7168
+ ...DEFAULT_DESCRIPTION,
7169
+ ...overrides
7170
+ };
7171
+ }
7172
+ function createTooltipContext(overrides = {}) {
7173
+ return {
7174
+ ...DEFAULT_TOOLTIP,
7175
+ ...overrides
7176
+ };
7177
+ }
7178
+
7179
+ // hooks //////////////////
7180
+
7181
+ /**
7182
+ * This hook behaves like useLayoutEffect, but does not trigger on the first render.
7183
+ *
7184
+ * @param {Function} effect
7185
+ * @param {Array} deps
7186
+ */
7187
+ function useUpdateLayoutEffect(effect, deps) {
7188
+ const isMounted = useRef$1(false);
7189
+ useLayoutEffect(() => {
7190
+ if (isMounted.current) {
7191
+ return effect();
7192
+ } else {
7193
+ isMounted.current = true;
7194
+ }
7195
+ }, deps);
7196
+ }
7197
+ function CollapsibleEntry(props) {
7198
+ const {
7199
+ element,
7200
+ entries = [],
7201
+ id,
7202
+ label,
7203
+ open: shouldOpen,
7204
+ remove
7205
+ } = props;
7206
+ const [open, setOpen] = useState(shouldOpen);
7207
+ const toggleOpen = () => setOpen(!open);
7208
+ const {
7209
+ onShow
7210
+ } = useContext(LayoutContext);
7211
+ const propertiesPanelContext = {
7212
+ ...useContext(LayoutContext),
7213
+ onShow: useCallback(() => {
7214
+ setOpen(true);
7215
+ if (isFunction(onShow)) {
7216
+ onShow();
7217
+ }
7218
+ }, [onShow, setOpen])
7219
+ };
7220
+
7221
+ // todo(pinussilvestrus): translate once we have a translate mechanism for the core
7222
+ const placeholderLabel = '<empty>';
7223
+ return jsxs("div", {
7224
+ "data-entry-id": id,
7225
+ class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
7226
+ children: [jsxs("div", {
7227
+ class: "bio-properties-panel-collapsible-entry-header",
7228
+ onClick: toggleOpen,
7229
+ children: [jsx("div", {
7230
+ title: label || placeholderLabel,
7231
+ class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
7232
+ children: label || placeholderLabel
7233
+ }), jsx("button", {
7234
+ title: "Toggle list item",
7235
+ class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
7236
+ children: jsx(ArrowIcon, {
7237
+ class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
7238
+ })
7239
+ }), remove ? jsx("button", {
7240
+ title: "Delete item",
7241
+ class: "bio-properties-panel-remove-entry",
7242
+ onClick: remove,
7243
+ children: jsx(DeleteIcon, {})
7244
+ }) : null]
7245
+ }), jsx("div", {
7246
+ class: classnames('bio-properties-panel-collapsible-entry-entries', open ? 'open' : ''),
7247
+ children: jsx(LayoutContext.Provider, {
7248
+ value: propertiesPanelContext,
7249
+ children: entries.map(entry => {
7250
+ const {
7251
+ component: Component,
7252
+ id
7253
+ } = entry;
7254
+ return createElement(Component, {
7255
+ ...entry,
7256
+ element: element,
7257
+ key: id
7258
+ });
7259
+ })
7260
+ })
7261
+ })]
7262
+ });
7263
+ }
7264
+ function ListItem(props) {
7265
+ const {
7266
+ autoFocusEntry,
7267
+ autoOpen
7268
+ } = props;
6715
7269
 
6716
- // To be consistent with the FEEL editor, set focus at start of input
6717
- // this ensures clean editing experience when switching with the keyboard
6718
- ref.current = {
6719
- focus: position => {
6720
- const input = inputRef.current;
6721
- if (!input) {
6722
- return;
6723
- }
6724
- input.focus();
6725
- if (typeof position === 'number' && position !== Infinity) {
6726
- if (position > value.length) {
6727
- position = value.length;
7270
+ // focus specified entry on auto open
7271
+ useEffect(() => {
7272
+ if (autoOpen && autoFocusEntry) {
7273
+ const entry = query(`[data-entry-id="${autoFocusEntry}"]`);
7274
+ const focusableInput = query('.bio-properties-panel-input', entry);
7275
+ if (focusableInput) {
7276
+ if (isFunction(focusableInput.select)) {
7277
+ focusableInput.select();
7278
+ } else if (isFunction(focusableInput.focus)) {
7279
+ focusableInput.focus();
6728
7280
  }
6729
- input.setSelectionRange(position, position);
6730
7281
  }
6731
7282
  }
6732
- };
6733
- return jsx(NumberField, {
6734
- id: id,
6735
- debounce: debounce,
6736
- disabled: disabled,
6737
- displayLabel: false,
6738
- inputRef: inputRef,
6739
- max: max,
6740
- min: min,
6741
- onInput: onInput,
6742
- step: step,
6743
- value: value,
6744
- onFocus: onFocus,
6745
- onBlur: onBlur
7283
+ }, [autoOpen, autoFocusEntry]);
7284
+ return jsx("div", {
7285
+ class: "bio-properties-panel-list-item",
7286
+ children: jsx(CollapsibleEntry, {
7287
+ ...props,
7288
+ open: autoOpen
7289
+ })
6746
7290
  });
6747
- });
6748
- forwardRef((props, ref) => {
7291
+ }
7292
+ const noop$1 = () => {};
7293
+
7294
+ /**
7295
+ * @param {import('../PropertiesPanel').ListGroupDefinition} props
7296
+ */
7297
+ function ListGroup(props) {
6749
7298
  const {
7299
+ add,
7300
+ element,
6750
7301
  id,
6751
- disabled,
6752
- onInput,
6753
- value,
6754
- onFocus,
6755
- onBlur
7302
+ items,
7303
+ label,
7304
+ shouldOpen = true,
7305
+ shouldSort = true
6756
7306
  } = props;
6757
- const inputRef = useRef$1();
7307
+ const groupRef = useRef$1(null);
7308
+ const [open, setOpen] = useLayoutState(['groups', id, 'open'], false);
7309
+ const [sticky, setSticky] = useState(false);
7310
+ const onShow = useCallback(() => setOpen(true), [setOpen]);
7311
+ const [ordering, setOrdering] = useState([]);
7312
+ const [newItemAdded, setNewItemAdded] = useState(false);
6758
7313
 
6759
- // To be consistent with the FEEL editor, set focus at start of input
6760
- // this ensures clean editing experience when switching with the keyboard
6761
- ref.current = {
6762
- focus: () => {
6763
- const input = inputRef.current;
6764
- if (!input) {
6765
- return;
7314
+ // Flag to mark that add button was clicked in the last render cycle
7315
+ const [addTriggered, setAddTriggered] = useState(false);
7316
+ const prevItems = usePrevious(items);
7317
+ const prevElement = usePrevious(element);
7318
+ const elementChanged = element !== prevElement;
7319
+ const shouldHandleEffects = !elementChanged && (shouldSort || shouldOpen);
7320
+
7321
+ // reset initial ordering when element changes (before first render)
7322
+ if (elementChanged) {
7323
+ setOrdering(createOrdering(shouldSort ? sortItems(items) : items));
7324
+ }
7325
+
7326
+ // keep ordering in sync to items - and open changes
7327
+
7328
+ // (0) set initial ordering from given items
7329
+ useEffect(() => {
7330
+ if (!prevItems || !shouldSort) {
7331
+ setOrdering(createOrdering(items));
7332
+ }
7333
+ }, [items, element]);
7334
+
7335
+ // (1) items were added
7336
+ useEffect(() => {
7337
+ // reset addTriggered flag
7338
+ setAddTriggered(false);
7339
+ if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
7340
+ let add = [];
7341
+ items.forEach(item => {
7342
+ if (!ordering.includes(item.id)) {
7343
+ add.push(item.id);
7344
+ }
7345
+ });
7346
+ let newOrdering = ordering;
7347
+
7348
+ // open if not open, configured and triggered by add button
7349
+ //
7350
+ // TODO(marstamm): remove once we refactor layout handling for listGroups.
7351
+ // Ideally, opening should be handled as part of the `add` callback and
7352
+ // not be a concern for the ListGroup component.
7353
+ if (addTriggered && !open && shouldOpen) {
7354
+ toggleOpen();
6766
7355
  }
6767
- input.focus();
6768
- input.setSelectionRange(0, 0);
7356
+
7357
+ // filter when not open and configured
7358
+ if (!open && shouldSort) {
7359
+ newOrdering = createOrdering(sortItems(items));
7360
+ }
7361
+
7362
+ // add new items on top or bottom depending on sorting behavior
7363
+ newOrdering = newOrdering.filter(item => !add.includes(item));
7364
+ if (shouldSort) {
7365
+ newOrdering.unshift(...add);
7366
+ } else {
7367
+ newOrdering.push(...add);
7368
+ }
7369
+ setOrdering(newOrdering);
7370
+ setNewItemAdded(addTriggered);
7371
+ } else {
7372
+ setNewItemAdded(false);
7373
+ }
7374
+ }, [items, open, shouldHandleEffects, addTriggered]);
7375
+
7376
+ // (2) sort items on open if shouldSort is set
7377
+ useEffect(() => {
7378
+ if (shouldSort && open && !newItemAdded) {
7379
+ setOrdering(createOrdering(sortItems(items)));
7380
+ }
7381
+ }, [open, shouldSort]);
7382
+
7383
+ // (3) items were deleted
7384
+ useEffect(() => {
7385
+ if (shouldHandleEffects && prevItems && items.length < prevItems.length) {
7386
+ let keep = [];
7387
+ ordering.forEach(o => {
7388
+ if (getItem(items, o)) {
7389
+ keep.push(o);
7390
+ }
7391
+ });
7392
+ setOrdering(keep);
6769
7393
  }
7394
+ }, [items, shouldHandleEffects]);
7395
+
7396
+ // set css class when group is sticky to top
7397
+ useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);
7398
+ const toggleOpen = () => setOpen(!open);
7399
+ const hasItems = !!items.length;
7400
+ const propertiesPanelContext = {
7401
+ ...useContext(LayoutContext),
7402
+ onShow
6770
7403
  };
6771
- return jsx("textarea", {
6772
- id: prefixId$4(id),
6773
- type: "text",
6774
- ref: inputRef,
6775
- name: id,
6776
- spellCheck: "false",
6777
- autoComplete: "off",
6778
- disabled: disabled,
6779
- class: "bio-properties-panel-input",
6780
- onInput: e => onInput(e.target.value),
6781
- onFocus: onFocus,
6782
- onBlur: onBlur,
6783
- value: value || '',
6784
- "data-gramm": "false"
7404
+ const handleAddClick = e => {
7405
+ setAddTriggered(true);
7406
+ add(e);
7407
+ };
7408
+ const allErrors = useErrors();
7409
+ const hasError = items.some(item => {
7410
+ if (allErrors[item.id]) {
7411
+ return true;
7412
+ }
7413
+ if (!item.entries) {
7414
+ return;
7415
+ }
7416
+
7417
+ // also check if the error is nested, e.g. for name-value entries
7418
+ return item.entries.some(entry => allErrors[entry.id]);
6785
7419
  });
6786
- });
6787
- const OptionalFeelToggleSwitch = forwardRef((props, ref) => {
6788
- const {
6789
- id,
6790
- onInput,
6791
- value,
6792
- onFocus,
6793
- onBlur,
6794
- switcherLabel
6795
- } = props;
6796
- const inputRef = useRef$1();
7420
+ return jsxs("div", {
7421
+ class: "bio-properties-panel-group",
7422
+ "data-group-id": 'group-' + id,
7423
+ ref: groupRef,
7424
+ children: [jsxs("div", {
7425
+ class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
7426
+ onClick: hasItems ? toggleOpen : noop$1,
7427
+ children: [jsx("div", {
7428
+ title: label,
7429
+ class: "bio-properties-panel-group-header-title",
7430
+ children: jsx(TooltipWrapper, {
7431
+ value: props.tooltip,
7432
+ forId: 'group-' + id,
7433
+ element: element,
7434
+ parent: groupRef,
7435
+ children: label
7436
+ })
7437
+ }), jsxs("div", {
7438
+ class: "bio-properties-panel-group-header-buttons",
7439
+ children: [add ? jsxs("button", {
7440
+ title: "Create new list item",
7441
+ class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
7442
+ onClick: handleAddClick,
7443
+ children: [jsx(CreateIcon, {}), !hasItems ? jsx("span", {
7444
+ class: "bio-properties-panel-add-entry-label",
7445
+ children: "Create"
7446
+ }) : null]
7447
+ }) : null, hasItems ? jsx("div", {
7448
+ title: `List contains ${items.length} item${items.length != 1 ? 's' : ''}`,
7449
+ class: classnames('bio-properties-panel-list-badge', hasError ? 'bio-properties-panel-list-badge--error' : ''),
7450
+ children: items.length
7451
+ }) : null, hasItems ? jsx("button", {
7452
+ title: "Toggle section",
7453
+ class: "bio-properties-panel-group-header-button bio-properties-panel-arrow",
7454
+ children: jsx(ArrowIcon, {
7455
+ class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
7456
+ })
7457
+ }) : null]
7458
+ })]
7459
+ }), jsx("div", {
7460
+ class: classnames('bio-properties-panel-list', open && hasItems ? 'open' : ''),
7461
+ children: jsx(LayoutContext.Provider, {
7462
+ value: propertiesPanelContext,
7463
+ children: ordering.map((o, index) => {
7464
+ const item = getItem(items, o);
7465
+ if (!item) {
7466
+ return;
7467
+ }
7468
+ const {
7469
+ id
7470
+ } = item;
6797
7471
 
6798
- // To be consistent with the FEEL editor, set focus at start of input
6799
- // this ensures clean editing experience when switching with the keyboard
6800
- ref.current = {
6801
- focus: () => {
6802
- const input = inputRef.current;
6803
- if (!input) {
6804
- return;
6805
- }
6806
- input.focus();
6807
- }
6808
- };
6809
- return jsx(ToggleSwitch, {
6810
- id: id,
6811
- value: value,
6812
- inputRef: inputRef,
6813
- onInput: onInput,
6814
- onFocus: onFocus,
6815
- onBlur: onBlur,
6816
- switcherLabel: switcherLabel
7472
+ // if item was added, open it
7473
+ // Existing items will not be affected as autoOpen is only applied on first render
7474
+ const autoOpen = newItemAdded;
7475
+ return createElement(ListItem, {
7476
+ ...item,
7477
+ autoOpen: autoOpen,
7478
+ element: element,
7479
+ index: index,
7480
+ key: id
7481
+ });
7482
+ })
7483
+ })
7484
+ })]
6817
7485
  });
6818
- });
6819
- forwardRef((props, ref) => {
6820
- const {
6821
- id,
6822
- disabled,
6823
- onInput,
6824
- value,
6825
- onFocus,
6826
- onBlur
6827
- } = props;
6828
- const inputRef = useRef$1();
6829
- const handleChange = ({
6830
- target
6831
- }) => {
6832
- onInput(target.checked);
6833
- };
7486
+ }
6834
7487
 
6835
- // To be consistent with the FEEL editor, set focus at start of input
6836
- // this ensures clean editing experience when switching with the keyboard
6837
- ref.current = {
6838
- focus: () => {
6839
- const input = inputRef.current;
6840
- if (!input) {
6841
- return;
6842
- }
6843
- input.focus();
6844
- }
6845
- };
6846
- return jsx("input", {
6847
- ref: inputRef,
6848
- id: prefixId$4(id),
6849
- name: id,
6850
- onFocus: onFocus,
6851
- onBlur: onBlur,
6852
- type: "checkbox",
6853
- class: "bio-properties-panel-input",
6854
- onChange: handleChange,
6855
- checked: value,
6856
- disabled: disabled
6857
- });
6858
- });
7488
+ // helpers ////////////////////
6859
7489
 
6860
7490
  /**
6861
- * @param {Object} props
6862
- * @param {Object} props.element
6863
- * @param {String} props.id
6864
- * @param {String} props.description
6865
- * @param {Boolean} props.debounce
6866
- * @param {Boolean} props.disabled
6867
- * @param {Boolean} props.feel
6868
- * @param {String} props.label
6869
- * @param {Function} props.getValue
6870
- * @param {Function} props.setValue
6871
- * @param {Function} props.tooltipContainer
6872
- * @param {Function} props.validate
6873
- * @param {Function} props.show
6874
- * @param {Function} props.example
6875
- * @param {Function} props.variables
6876
- * @param {Function} props.onFocus
6877
- * @param {Function} props.onBlur
6878
- * @param {string|import('preact').Component} props.tooltip
7491
+ * Sorts given items alphanumeric by label
6879
7492
  */
6880
- function FeelEntry(props) {
7493
+ function sortItems(items) {
7494
+ return sortBy(items, i => i.label.toLowerCase());
7495
+ }
7496
+ function getItem(items, id) {
7497
+ return find(items, i => i.id === id);
7498
+ }
7499
+ function createOrdering(items) {
7500
+ return items.map(i => i.id);
7501
+ }
7502
+ function Checkbox(props) {
6881
7503
  const {
6882
- element,
6883
7504
  id,
6884
- description,
6885
- debounce,
6886
- disabled,
6887
- feel,
6888
7505
  label,
6889
- getValue,
6890
- setValue,
6891
- tooltipContainer,
6892
- hostLanguage,
6893
- singleLine,
6894
- validate,
6895
- show = noop$1,
6896
- example,
6897
- variables,
7506
+ onChange,
7507
+ disabled,
7508
+ value = false,
6898
7509
  onFocus,
6899
7510
  onBlur,
6900
7511
  tooltip
6901
7512
  } = props;
6902
- const [validationError, setValidationError] = useState(null);
6903
- const [localError, setLocalError] = useState(null);
6904
- let value = getValue(element);
7513
+ const [localValue, setLocalValue] = useState(value);
7514
+ const handleChangeCallback = ({
7515
+ target
7516
+ }) => {
7517
+ onChange(target.checked);
7518
+ };
7519
+ const handleChange = e => {
7520
+ handleChangeCallback(e);
7521
+ setLocalValue(e.target.value);
7522
+ };
6905
7523
  useEffect(() => {
6906
- if (isFunction(validate)) {
6907
- const newValidationError = validate(value) || null;
6908
- setValidationError(newValidationError);
7524
+ if (value === localValue) {
7525
+ return;
6909
7526
  }
7527
+ setLocalValue(value);
6910
7528
  }, [value]);
6911
- const onInput = useStaticCallback(newValue => {
6912
- let newValidationError = null;
6913
- if (isFunction(validate)) {
6914
- newValidationError = validate(newValue) || null;
6915
- }
6916
-
6917
- // don't create multiple commandStack entries for the same value
6918
- if (newValue !== value) {
6919
- setValue(newValue, newValidationError);
6920
- }
6921
- setValidationError(newValidationError);
6922
- });
6923
- const onError = useCallback(err => {
6924
- setLocalError(err);
6925
- }, []);
6926
- const temporaryError = useError(id);
6927
- const error = localError || temporaryError || validationError;
7529
+ const ref = useShowEntryEvent(id);
6928
7530
  return jsxs("div", {
6929
- class: classnames(props.class, 'bio-properties-panel-entry', error ? 'has-error' : ''),
6930
- "data-entry-id": id,
6931
- children: [createElement(FeelTextfield, {
6932
- ...props,
6933
- debounce: debounce,
6934
- disabled: disabled,
6935
- feel: feel,
6936
- id: id,
6937
- key: element,
6938
- label: label,
6939
- onInput: onInput,
6940
- onError: onError,
7531
+ class: "bio-properties-panel-checkbox",
7532
+ children: [jsx("input", {
7533
+ ref: ref,
7534
+ id: prefixId$4(id),
7535
+ name: id,
6941
7536
  onFocus: onFocus,
6942
7537
  onBlur: onBlur,
6943
- example: example,
6944
- hostLanguage: hostLanguage,
6945
- singleLine: singleLine,
6946
- show: show,
6947
- value: value,
6948
- variables: variables,
6949
- tooltipContainer: tooltipContainer,
6950
- OptionalComponent: props.OptionalComponent,
6951
- tooltip: tooltip
6952
- }), error && jsx("div", {
6953
- class: "bio-properties-panel-error",
6954
- children: error
6955
- }), jsx(Description$1, {
6956
- forId: id,
6957
- element: element,
6958
- value: description
6959
- })]
6960
- });
6961
- }
6962
-
6963
- /**
6964
- * @param {Object} props
6965
- * @param {Object} props.element
6966
- * @param {String} props.id
6967
- * @param {String} props.description
6968
- * @param {Boolean} props.debounce
6969
- * @param {Boolean} props.disabled
6970
- * @param {String} props.max
6971
- * @param {String} props.min
6972
- * @param {String} props.step
6973
- * @param {Boolean} props.feel
6974
- * @param {String} props.label
6975
- * @param {Function} props.getValue
6976
- * @param {Function} props.setValue
6977
- * @param {Function} props.tooltipContainer
6978
- * @param {Function} props.validate
6979
- * @param {Function} props.show
6980
- * @param {Function} props.example
6981
- * @param {Function} props.variables
6982
- * @param {Function} props.onFocus
6983
- * @param {Function} props.onBlur
6984
- */
6985
- function FeelNumberEntry(props) {
6986
- return jsx(FeelEntry, {
6987
- class: "bio-properties-panel-feel-number",
6988
- OptionalComponent: OptionalFeelNumberField,
6989
- ...props
6990
- });
6991
- }
6992
-
6993
- /**
6994
- * @param {Object} props
6995
- * @param {Object} props.element
6996
- * @param {String} props.id
6997
- * @param {String} props.description
6998
- * @param {Boolean} props.debounce
6999
- * @param {Boolean} props.disabled
7000
- * @param {Boolean} props.feel
7001
- * @param {String} props.label
7002
- * @param {Function} props.getValue
7003
- * @param {Function} props.setValue
7004
- * @param {Function} props.tooltipContainer
7005
- * @param {Function} props.validate
7006
- * @param {Function} props.show
7007
- * @param {Function} props.example
7008
- * @param {Function} props.variables
7009
- * @param {Function} props.onFocus
7010
- * @param {Function} props.onBlur
7011
- */
7012
- function FeelToggleSwitchEntry(props) {
7013
- return jsx(FeelEntry, {
7014
- class: "bio-properties-panel-feel-toggle-switch",
7015
- OptionalComponent: OptionalFeelToggleSwitch,
7016
- ...props
7538
+ type: "checkbox",
7539
+ class: "bio-properties-panel-input",
7540
+ onChange: handleChange,
7541
+ checked: localValue,
7542
+ disabled: disabled
7543
+ }), jsx("label", {
7544
+ for: prefixId$4(id),
7545
+ class: "bio-properties-panel-label",
7546
+ children: jsx(TooltipWrapper, {
7547
+ value: tooltip,
7548
+ forId: id,
7549
+ element: props.element,
7550
+ children: label
7551
+ })
7552
+ })]
7017
7553
  });
7018
7554
  }
7019
7555
 
@@ -7022,37 +7558,54 @@ function FeelToggleSwitchEntry(props) {
7022
7558
  * @param {Object} props.element
7023
7559
  * @param {String} props.id
7024
7560
  * @param {String} props.description
7025
- * @param {String} props.hostLanguage
7026
- * @param {Boolean} props.singleLine
7027
- * @param {Boolean} props.debounce
7028
- * @param {Boolean} props.disabled
7029
- * @param {Boolean} props.feel
7030
7561
  * @param {String} props.label
7031
7562
  * @param {Function} props.getValue
7032
7563
  * @param {Function} props.setValue
7033
- * @param {Function} props.tooltipContainer
7034
- * @param {Function} props.validate
7035
- * @param {Function} props.show
7036
- * @param {Function} props.example
7037
- * @param {Function} props.variables
7038
7564
  * @param {Function} props.onFocus
7039
7565
  * @param {Function} props.onBlur
7566
+ * @param {string|import('preact').Component} props.tooltip
7567
+ * @param {boolean} [props.disabled]
7040
7568
  */
7041
- function FeelTemplatingEntry(props) {
7042
- return jsx(FeelEntry, {
7043
- class: "bio-properties-panel-feel-templating",
7044
- OptionalComponent: CodeEditor$1,
7045
- ...props
7569
+ function CheckboxEntry(props) {
7570
+ const {
7571
+ element,
7572
+ id,
7573
+ description,
7574
+ label,
7575
+ getValue,
7576
+ setValue,
7577
+ disabled,
7578
+ onFocus,
7579
+ onBlur,
7580
+ tooltip
7581
+ } = props;
7582
+ const value = getValue(element);
7583
+ const error = useError(id);
7584
+ return jsxs("div", {
7585
+ class: "bio-properties-panel-entry bio-properties-panel-checkbox-entry",
7586
+ "data-entry-id": id,
7587
+ children: [jsx(Checkbox, {
7588
+ disabled: disabled,
7589
+ id: id,
7590
+ label: label,
7591
+ onChange: setValue,
7592
+ onFocus: onFocus,
7593
+ onBlur: onBlur,
7594
+ value: value,
7595
+ tooltip: tooltip,
7596
+ element: element
7597
+ }, element), error && jsx("div", {
7598
+ class: "bio-properties-panel-error",
7599
+ children: error
7600
+ }), jsx(Description$1, {
7601
+ forId: id,
7602
+ element: element,
7603
+ value: description
7604
+ })]
7046
7605
  });
7047
7606
  }
7048
7607
  function isEdited$5(node) {
7049
- if (!node) {
7050
- return false;
7051
- }
7052
- if (node.type === 'checkbox') {
7053
- return !!node.checked || node.classList.contains('edited');
7054
- }
7055
- return !!node.value || node.classList.contains('edited');
7608
+ return node && !!node.checked;
7056
7609
  }
7057
7610
 
7058
7611
  // helpers /////////////////
@@ -7548,6 +8101,9 @@ function textToLabel(text) {
7548
8101
  }
7549
8102
  return null;
7550
8103
  }
8104
+ function isValidDotPath(path) {
8105
+ return /^\w+(\.\w+)*$/.test(path);
8106
+ }
7551
8107
  const INPUTS = ['checkbox', 'checklist', 'datetime', 'number', 'radio', 'select', 'taglist', 'textfield', 'textarea'];
7552
8108
  const VALUES_INPUTS = ['checklist', 'radio', 'select', 'taglist'];
7553
8109
 
@@ -7558,6 +8114,7 @@ const labelsByType = {
7558
8114
  columns: 'COLUMNS',
7559
8115
  default: 'FORM',
7560
8116
  datetime: 'DATETIME',
8117
+ group: 'GROUP',
7561
8118
  image: 'IMAGE VIEW',
7562
8119
  number: 'NUMBER',
7563
8120
  radio: 'RADIO',
@@ -7573,6 +8130,9 @@ const PropertiesPanelHeaderProvider = {
7573
8130
  const {
7574
8131
  type
7575
8132
  } = field;
8133
+ if (type === 'spacer') {
8134
+ return '';
8135
+ }
7576
8136
  if (type === 'text') {
7577
8137
  return textToLabel(field.text);
7578
8138
  }
@@ -7704,7 +8264,7 @@ function AltTextEntry(props) {
7704
8264
  component: AltText,
7705
8265
  editField: editField,
7706
8266
  field: field,
7707
- isEdited: isEdited$5
8267
+ isEdited: isEdited$6
7708
8268
  });
7709
8269
  }
7710
8270
  return entries;
@@ -7827,7 +8387,7 @@ function DescriptionEntry(props) {
7827
8387
  component: Description,
7828
8388
  editField: editField,
7829
8389
  field: field,
7830
- isEdited: isEdited$5
8390
+ isEdited: isEdited$6
7831
8391
  });
7832
8392
  }
7833
8393
  return entries;
@@ -8114,7 +8674,7 @@ function DisabledEntry(props) {
8114
8674
  component: Disabled,
8115
8675
  editField: editField,
8116
8676
  field: field,
8117
- isEdited: isEdited$7
8677
+ isEdited: isEdited$8
8118
8678
  });
8119
8679
  }
8120
8680
  return entries;
@@ -8249,7 +8809,7 @@ function Key$1(props) {
8249
8809
  field,
8250
8810
  id
8251
8811
  } = props;
8252
- const formFieldRegistry = useService('formFieldRegistry');
8812
+ const pathRegistry = useService('pathRegistry');
8253
8813
  const debounce = useService('debounce');
8254
8814
  const path = ['key'];
8255
8815
  const getValue = () => {
@@ -8262,17 +8822,32 @@ function Key$1(props) {
8262
8822
  return editField(field, path, value);
8263
8823
  };
8264
8824
  const validate = value => {
8825
+ if (value === field.key) {
8826
+ return null;
8827
+ }
8265
8828
  if (isUndefined(value) || !value.length) {
8266
8829
  return 'Must not be empty.';
8267
8830
  }
8268
- if (/\s/.test(value)) {
8269
- return 'Must not contain spaces.';
8831
+ if (value && !isValidDotPath(value)) {
8832
+ return 'Must be a variable or a dot separated path.';
8270
8833
  }
8271
- const assigned = formFieldRegistry._keys.assigned(value);
8272
- if (assigned && assigned !== field) {
8273
- return 'Must be unique.';
8834
+ const hasIntegerPathSegment = value.split('.').some(segment => /^\d+$/.test(segment));
8835
+ if (hasIntegerPathSegment) {
8836
+ return 'Must not contain numerical path segments.';
8274
8837
  }
8275
- return null;
8838
+ const replacements = {
8839
+ [field.id]: value.split('.')
8840
+ };
8841
+ const oldPath = pathRegistry.getValuePath(field);
8842
+ const newPath = pathRegistry.getValuePath(field, {
8843
+ replacements
8844
+ });
8845
+
8846
+ // unclaim temporarily to avoid self-conflicts
8847
+ pathRegistry.unclaimPath(oldPath);
8848
+ const canClaim = pathRegistry.canClaimPath(newPath, true);
8849
+ pathRegistry.claimPath(oldPath, true);
8850
+ return canClaim ? null : 'Must not conflict with other key/path assignments.';
8276
8851
  };
8277
8852
  return TextfieldEntry({
8278
8853
  debounce,
@@ -8287,6 +8862,147 @@ function Key$1(props) {
8287
8862
  });
8288
8863
  }
8289
8864
 
8865
+ function PathEntry(props) {
8866
+ const {
8867
+ editField,
8868
+ field
8869
+ } = props;
8870
+ const {
8871
+ type
8872
+ } = field;
8873
+ const entries = [];
8874
+ if (type === 'group') {
8875
+ entries.push({
8876
+ id: 'path',
8877
+ component: Path,
8878
+ editField: editField,
8879
+ field: field,
8880
+ isEdited: isEdited
8881
+ });
8882
+ }
8883
+ return entries;
8884
+ }
8885
+ function Path(props) {
8886
+ const {
8887
+ editField,
8888
+ field,
8889
+ id
8890
+ } = props;
8891
+ const debounce = useService('debounce');
8892
+ const pathRegistry = useService('pathRegistry');
8893
+ const path = ['path'];
8894
+ const getValue = () => {
8895
+ return get(field, path, '');
8896
+ };
8897
+ const setValue = (value, error) => {
8898
+ if (error) {
8899
+ return;
8900
+ }
8901
+ return editField(field, path, value);
8902
+ };
8903
+ const validate = value => {
8904
+ if (!value || value === field.path) {
8905
+ return null;
8906
+ }
8907
+ if (value && !isValidDotPath(value)) {
8908
+ return 'Must be empty, a variable or a dot separated path';
8909
+ }
8910
+ const hasIntegerPathSegment = value && value.split('.').some(segment => /^\d+$/.test(segment));
8911
+ if (hasIntegerPathSegment) {
8912
+ return 'Must not contain numerical path segments.';
8913
+ }
8914
+ const options = value && {
8915
+ replacements: {
8916
+ [field.id]: [value]
8917
+ }
8918
+ } || {};
8919
+ const canClaim = pathRegistry.executeRecursivelyOnFields(field, ({
8920
+ field,
8921
+ isClosed
8922
+ }) => {
8923
+ const path = pathRegistry.getValuePath(field, options);
8924
+ return pathRegistry.canClaimPath(path, isClosed);
8925
+ });
8926
+ if (!canClaim) {
8927
+ return 'Must not cause two binding paths to colide';
8928
+ }
8929
+ };
8930
+ return TextfieldEntry({
8931
+ debounce,
8932
+ description: 'Where the child variables of this component are pathed to.',
8933
+ element: field,
8934
+ getValue,
8935
+ id,
8936
+ label: 'Path',
8937
+ tooltip: 'Routes the children of this component into a form variable, may be left empty to route at the root level.',
8938
+ setValue,
8939
+ validate
8940
+ });
8941
+ }
8942
+
8943
+ function simpleBoolEntryFactory(options) {
8944
+ const {
8945
+ id,
8946
+ label,
8947
+ description,
8948
+ path,
8949
+ props
8950
+ } = options;
8951
+ const {
8952
+ editField,
8953
+ field
8954
+ } = props;
8955
+ return {
8956
+ id,
8957
+ label,
8958
+ path,
8959
+ field,
8960
+ editField,
8961
+ description,
8962
+ component: SimpleBoolComponent,
8963
+ isEdited: isEdited$5
8964
+ };
8965
+ }
8966
+ const SimpleBoolComponent = props => {
8967
+ const {
8968
+ id,
8969
+ label,
8970
+ path,
8971
+ field,
8972
+ editField,
8973
+ description
8974
+ } = props;
8975
+ const getValue = () => get(field, path, '');
8976
+ const setValue = value => editField(field, path, value || false);
8977
+ return CheckboxEntry({
8978
+ element: field,
8979
+ getValue,
8980
+ id,
8981
+ label,
8982
+ setValue,
8983
+ description
8984
+ });
8985
+ };
8986
+
8987
+ function GroupEntries(props) {
8988
+ const {
8989
+ field
8990
+ } = props;
8991
+ const {
8992
+ type
8993
+ } = field;
8994
+ if (type !== 'group') {
8995
+ return [];
8996
+ }
8997
+ const entries = [simpleBoolEntryFactory({
8998
+ id: 'showOutline',
8999
+ path: ['showOutline'],
9000
+ label: 'Show outline',
9001
+ props
9002
+ })];
9003
+ return entries;
9004
+ }
9005
+
8290
9006
  function LabelEntry(props) {
8291
9007
  const {
8292
9008
  field,
@@ -8304,7 +9020,7 @@ function LabelEntry(props) {
8304
9020
  component: DateLabel,
8305
9021
  editField,
8306
9022
  field,
8307
- isEdited: isEdited$5
9023
+ isEdited: isEdited$6
8308
9024
  });
8309
9025
  }
8310
9026
  if (subtype === DATETIME_SUBTYPES.TIME || subtype === DATETIME_SUBTYPES.DATETIME) {
@@ -8313,16 +9029,16 @@ function LabelEntry(props) {
8313
9029
  component: TimeLabel,
8314
9030
  editField,
8315
9031
  field,
8316
- isEdited: isEdited$5
9032
+ isEdited: isEdited$6
8317
9033
  });
8318
9034
  }
8319
- } else if (INPUTS.includes(type) || type === 'button') {
9035
+ } else if (INPUTS.includes(type) || type === 'button' || type === 'group') {
8320
9036
  entries.push({
8321
9037
  id: 'label',
8322
9038
  component: Label$1,
8323
9039
  editField,
8324
9040
  field,
8325
- isEdited: isEdited$5
9041
+ isEdited: isEdited$6
8326
9042
  });
8327
9043
  }
8328
9044
  return entries;
@@ -8344,12 +9060,13 @@ function Label$1(props) {
8344
9060
  const setValue = value => {
8345
9061
  return editField(field, path, value || '');
8346
9062
  };
9063
+ const label = field.type === 'group' ? 'Group label' : 'Field label';
8347
9064
  return FeelTemplatingEntry({
8348
9065
  debounce,
8349
9066
  element: field,
8350
9067
  getValue,
8351
9068
  id,
8352
- label: 'Field label',
9069
+ label,
8353
9070
  singleLine: true,
8354
9071
  setValue,
8355
9072
  variables
@@ -8427,7 +9144,7 @@ function SourceEntry(props) {
8427
9144
  component: Source,
8428
9145
  editField: editField,
8429
9146
  field: field,
8430
- isEdited: isEdited$5
9147
+ isEdited: isEdited$6
8431
9148
  });
8432
9149
  }
8433
9150
  return entries;
@@ -8484,7 +9201,7 @@ function TextEntry(props) {
8484
9201
  component: Text,
8485
9202
  editField: editField,
8486
9203
  field: field,
8487
- isEdited: isEdited$5
9204
+ isEdited: isEdited$6
8488
9205
  }];
8489
9206
 
8490
9207
  // todo: skipped to make the release without too much risk
@@ -8553,7 +9270,7 @@ function SpacerEntry(props) {
8553
9270
  entries.push({
8554
9271
  id: id + '-height',
8555
9272
  component: SpacerHeight,
8556
- isEdited: isEdited$6,
9273
+ isEdited: isEdited$7,
8557
9274
  editField,
8558
9275
  field
8559
9276
  });
@@ -8604,7 +9321,7 @@ function NumberEntries(props) {
8604
9321
  entries.push({
8605
9322
  id: id + '-decimalDigits',
8606
9323
  component: NumberDecimalDigits,
8607
- isEdited: isEdited$6,
9324
+ isEdited: isEdited$7,
8608
9325
  editField,
8609
9326
  field
8610
9327
  });
@@ -8708,7 +9425,7 @@ function NumberSerializationEntry(props) {
8708
9425
  entries.push({
8709
9426
  id: 'serialize-to-string',
8710
9427
  component: SerializeToString,
8711
- isEdited: isEdited$8,
9428
+ isEdited: isEdited$5,
8712
9429
  editField,
8713
9430
  field
8714
9431
  });
@@ -8767,7 +9484,7 @@ function DateTimeEntry(props) {
8767
9484
  entries.push({
8768
9485
  id: 'use24h',
8769
9486
  component: Use24h,
8770
- isEdited: isEdited$8,
9487
+ isEdited: isEdited$5,
8771
9488
  editField,
8772
9489
  field
8773
9490
  });
@@ -8880,7 +9597,7 @@ function DateTimeConstraintsEntry(props) {
8880
9597
  entries.push({
8881
9598
  id: id + '-disallowPassedDates',
8882
9599
  component: DisallowPassedDates,
8883
- isEdited: isEdited$8,
9600
+ isEdited: isEdited$5,
8884
9601
  editField,
8885
9602
  field
8886
9603
  });
@@ -8981,50 +9698,6 @@ function TimeFormatSelect(props) {
8981
9698
  });
8982
9699
  }
8983
9700
 
8984
- function simpleBoolEntryFactory(options) {
8985
- const {
8986
- id,
8987
- label,
8988
- description,
8989
- path,
8990
- props
8991
- } = options;
8992
- const {
8993
- editField,
8994
- field
8995
- } = props;
8996
- return {
8997
- id,
8998
- label,
8999
- path,
9000
- field,
9001
- editField,
9002
- description,
9003
- component: SimpleBoolComponent,
9004
- isEdited: isEdited$8
9005
- };
9006
- }
9007
- const SimpleBoolComponent = props => {
9008
- const {
9009
- id,
9010
- label,
9011
- path,
9012
- field,
9013
- editField,
9014
- description
9015
- } = props;
9016
- const getValue = () => get(field, path, '');
9017
- const setValue = value => editField(field, path, value);
9018
- return CheckboxEntry({
9019
- element: field,
9020
- getValue,
9021
- id,
9022
- label,
9023
- setValue,
9024
- description
9025
- });
9026
- };
9027
-
9028
9701
  function SelectEntries(props) {
9029
9702
  const {
9030
9703
  field
@@ -9506,7 +10179,7 @@ function AdornerEntry(props) {
9506
10179
  entries.push({
9507
10180
  id: 'prefix-adorner',
9508
10181
  component: PrefixAdorner,
9509
- isEdited: isEdited$5,
10182
+ isEdited: isEdited$6,
9510
10183
  editField,
9511
10184
  field,
9512
10185
  onChange,
@@ -9515,7 +10188,7 @@ function AdornerEntry(props) {
9515
10188
  entries.push({
9516
10189
  id: 'suffix-adorner',
9517
10190
  component: SuffixAdorner,
9518
- isEdited: isEdited$5,
10191
+ isEdited: isEdited$6,
9519
10192
  editField,
9520
10193
  field,
9521
10194
  onChange,
@@ -9585,7 +10258,7 @@ function ReadonlyEntry(props) {
9585
10258
  component: Readonly,
9586
10259
  editField: editField,
9587
10260
  field: field,
9588
- isEdited: isEdited$5
10261
+ isEdited: isEdited$6
9589
10262
  });
9590
10263
  }
9591
10264
  return entries;
@@ -9630,7 +10303,7 @@ function ConditionEntry(props) {
9630
10303
  component: Condition,
9631
10304
  editField: editField,
9632
10305
  field: field,
9633
- isEdited: isEdited$5
10306
+ isEdited: isEdited$6
9634
10307
  }];
9635
10308
  }
9636
10309
  function Condition(props) {
@@ -9678,7 +10351,7 @@ function ValuesExpressionEntry(props) {
9678
10351
  id: id + '-expression',
9679
10352
  component: ValuesExpression,
9680
10353
  label: 'Values expression',
9681
- isEdited: isEdited$5,
10354
+ isEdited: isEdited$6,
9682
10355
  editField,
9683
10356
  field
9684
10357
  }];
@@ -9730,6 +10403,12 @@ function GeneralGroup(field, editField, getService) {
9730
10403
  }), ...KeyEntry({
9731
10404
  field,
9732
10405
  editField
10406
+ }), ...PathEntry({
10407
+ field,
10408
+ editField
10409
+ }), ...GroupEntries({
10410
+ field,
10411
+ editField
9733
10412
  }), ...DefaultOptionEntry({
9734
10413
  field,
9735
10414
  editField
@@ -9844,7 +10523,7 @@ function ValidationGroup(field, editField) {
9844
10523
  component: Required,
9845
10524
  getValue,
9846
10525
  field,
9847
- isEdited: isEdited$8,
10526
+ isEdited: isEdited$5,
9848
10527
  onChange
9849
10528
  }];
9850
10529
  if (type === 'textfield') {
@@ -9864,14 +10543,14 @@ function ValidationGroup(field, editField) {
9864
10543
  component: MinLength,
9865
10544
  getValue,
9866
10545
  field,
9867
- isEdited: isEdited$5,
10546
+ isEdited: isEdited$6,
9868
10547
  onChange
9869
10548
  }, {
9870
10549
  id: 'maxLength',
9871
10550
  component: MaxLength,
9872
10551
  getValue,
9873
10552
  field,
9874
- isEdited: isEdited$5,
10553
+ isEdited: isEdited$6,
9875
10554
  onChange
9876
10555
  });
9877
10556
  }
@@ -9891,14 +10570,14 @@ function ValidationGroup(field, editField) {
9891
10570
  component: Min,
9892
10571
  getValue,
9893
10572
  field,
9894
- isEdited: isEdited$5,
10573
+ isEdited: isEdited$6,
9895
10574
  onChange
9896
10575
  }, {
9897
10576
  id: 'max',
9898
10577
  component: Max,
9899
10578
  getValue,
9900
10579
  field,
9901
- isEdited: isEdited$5,
10580
+ isEdited: isEdited$6,
9902
10581
  onChange
9903
10582
  });
9904
10583
  }