@bpmn-io/form-js-editor 1.15.4 → 1.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -3108,7 +3108,7 @@ function hasModifier(event) {
3108
3108
  * @param {KeyboardEvent} event
3109
3109
  * @return {boolean}
3110
3110
  */
3111
- function isCmd(event) {
3111
+ function isCmd$1(event) {
3112
3112
  // ensure we don't react to AltGr
3113
3113
  // (mapped to CTRL + ALT)
3114
3114
  if (event.altKey) {
@@ -3140,28 +3140,28 @@ function isShift(event) {
3140
3140
  * @param {KeyboardEvent} event
3141
3141
  */
3142
3142
  function isCopy(event) {
3143
- return isCmd(event) && isKey(KEYS_COPY, event);
3143
+ return isCmd$1(event) && isKey(KEYS_COPY, event);
3144
3144
  }
3145
3145
 
3146
3146
  /**
3147
3147
  * @param {KeyboardEvent} event
3148
3148
  */
3149
3149
  function isPaste(event) {
3150
- return isCmd(event) && isKey(KEYS_PASTE, event);
3150
+ return isCmd$1(event) && isKey(KEYS_PASTE, event);
3151
3151
  }
3152
3152
 
3153
3153
  /**
3154
3154
  * @param {KeyboardEvent} event
3155
3155
  */
3156
3156
  function isUndo(event) {
3157
- return isCmd(event) && !isShift(event) && isKey(KEYS_UNDO, event);
3157
+ return isCmd$1(event) && !isShift(event) && isKey(KEYS_UNDO, event);
3158
3158
  }
3159
3159
 
3160
3160
  /**
3161
3161
  * @param {KeyboardEvent} event
3162
3162
  */
3163
3163
  function isRedo(event) {
3164
- return isCmd(event) && (isKey(KEYS_REDO, event) || isKey(KEYS_UNDO, event) && isShift(event));
3164
+ return isCmd$1(event) && (isKey(KEYS_REDO, event) || isKey(KEYS_UNDO, event) && isShift(event));
3165
3165
  }
3166
3166
 
3167
3167
  /**
@@ -3330,7 +3330,7 @@ Keyboard.prototype.removeListener = function (listener, type) {
3330
3330
  this._eventBus.off(type || KEYDOWN_EVENT, listener);
3331
3331
  };
3332
3332
  Keyboard.prototype.hasModifier = hasModifier;
3333
- Keyboard.prototype.isCmd = isCmd;
3333
+ Keyboard.prototype.isCmd = isCmd$1;
3334
3334
  Keyboard.prototype.isShift = isShift;
3335
3335
  Keyboard.prototype.isKey = isKey;
3336
3336
 
@@ -3422,7 +3422,7 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3422
3422
 
3423
3423
  // quirk: it has to be triggered by `=` as well to work on international keyboard layout
3424
3424
  // cf: https://github.com/bpmn-io/bpmn-js/issues/1362#issuecomment-722989754
3425
- if (isKey(['+', 'Add', '='], event) && isCmd(event)) {
3425
+ if (isKey(['+', 'Add', '='], event) && isCmd$1(event)) {
3426
3426
  editorActions.trigger('stepZoom', {
3427
3427
  value: 1
3428
3428
  });
@@ -3434,7 +3434,7 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3434
3434
  // CTRL + -
3435
3435
  addListener('stepZoom', function (context) {
3436
3436
  var event = context.keyEvent;
3437
- if (isKey(['-', 'Subtract'], event) && isCmd(event)) {
3437
+ if (isKey(['-', 'Subtract'], event) && isCmd$1(event)) {
3438
3438
  editorActions.trigger('stepZoom', {
3439
3439
  value: -1
3440
3440
  });
@@ -3446,7 +3446,7 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3446
3446
  // CTRL + 0
3447
3447
  addListener('zoom', function (context) {
3448
3448
  var event = context.keyEvent;
3449
- if (isKey('0', event) && isCmd(event)) {
3449
+ if (isKey('0', event) && isCmd$1(event)) {
3450
3450
  editorActions.trigger('zoom', {
3451
3451
  value: 1
3452
3452
  });
@@ -5453,6 +5453,21 @@ OpenPopupIcon.defaultProps = {
5453
5453
  xmlns: "http://www.w3.org/2000/svg",
5454
5454
  viewBox: "0 0 16 16"
5455
5455
  };
5456
+
5457
+ /**
5458
+ * @typedef { {
5459
+ * getElementLabel: (element: object) => string,
5460
+ * getTypeLabel: (element: object) => string,
5461
+ * getElementIcon: (element: object) => import('preact').Component,
5462
+ * getDocumentationRef: (element: object) => string
5463
+ * } } HeaderProvider
5464
+ */
5465
+
5466
+ /**
5467
+ * @param {Object} props
5468
+ * @param {Object} props.element,
5469
+ * @param {HeaderProvider} props.headerProvider
5470
+ */
5456
5471
  function Header(props) {
5457
5472
  const {
5458
5473
  element,
@@ -5480,11 +5495,9 @@ function Header(props) {
5480
5495
  }), jsxs("div", {
5481
5496
  class: "bio-properties-panel-header-labels",
5482
5497
  children: [jsx("div", {
5483
- title: type,
5484
5498
  class: "bio-properties-panel-header-type",
5485
5499
  children: type
5486
5500
  }), label ? jsx("div", {
5487
- title: label,
5488
5501
  class: "bio-properties-panel-header-label",
5489
5502
  children: label
5490
5503
  }) : null]
@@ -5572,6 +5585,27 @@ function useTooltipContext(id, element) {
5572
5585
  } = useContext(TooltipContext);
5573
5586
  return getTooltipForId(id, element);
5574
5587
  }
5588
+
5589
+ /**
5590
+ * @typedef {Object} TooltipProps
5591
+ * @property {Object} [parent] - Parent element ref for portal rendering
5592
+ * @property {String} [direction='right'] - Tooltip direction ( 'right', 'top')
5593
+ * @property {String} [position] - Custom CSS position override
5594
+ * @property {Number} [showDelay=250] - Delay in ms before showing tooltip on hover
5595
+ * @property {Number} [hideDelay=250] - Delay in ms before hiding tooltip when mouse leaves, to avoid multiple tooltips from being opened, this should be the same as showDelay
5596
+ * @property {*} [children] - Child elements to render inside the tooltip wrapper
5597
+ */
5598
+
5599
+ /**
5600
+ * Tooltip wrapper that provides context-based tooltip content lookup.
5601
+ * All props are forwarded to the underlying Tooltip component.
5602
+ *
5603
+ * @param {TooltipProps & {
5604
+ * forId: String,
5605
+ * value?: String|Object,
5606
+ * element?: Object
5607
+ * }} props - Shared tooltip props plus wrapper-specific ones
5608
+ */
5575
5609
  function TooltipWrapper(props) {
5576
5610
  const {
5577
5611
  forId,
@@ -5588,35 +5622,93 @@ function TooltipWrapper(props) {
5588
5622
  forId: `bio-properties-panel-${forId}`
5589
5623
  });
5590
5624
  }
5625
+
5626
+ /**
5627
+ * @param {TooltipProps & {
5628
+ * forId: String,
5629
+ * value: String|Object
5630
+ * }} props
5631
+ */
5591
5632
  function Tooltip(props) {
5592
5633
  const {
5593
5634
  forId,
5594
5635
  value,
5595
5636
  parent,
5596
5637
  direction = 'right',
5597
- position
5638
+ position,
5639
+ showDelay = 250,
5640
+ hideDelay = 250
5598
5641
  } = props;
5599
5642
  const [visible, setVisible] = useState(false);
5600
-
5601
- // Tooltip will be shown after SHOW_DELAY ms from hovering over the source element.
5602
- const SHOW_DELAY = 200;
5603
- let timeout = null;
5643
+ const [tooltipPosition, setTooltipPosition] = useState(null);
5644
+ const [arrowOffset, setArrowOffset] = useState(null);
5645
+ const showTimeoutRef = useRef(null);
5646
+ const hideTimeoutRef = useRef(null);
5604
5647
  const wrapperRef = useRef(null);
5605
5648
  const tooltipRef = useRef(null);
5606
5649
  const show = (_, delay) => {
5650
+ clearTimeout(showTimeoutRef.current);
5651
+ clearTimeout(hideTimeoutRef.current);
5607
5652
  if (visible) return;
5608
5653
  if (delay) {
5609
- timeout = setTimeout(() => {
5654
+ showTimeoutRef.current = setTimeout(() => {
5610
5655
  setVisible(true);
5611
- }, SHOW_DELAY);
5656
+ }, showDelay);
5612
5657
  } else {
5613
5658
  setVisible(true);
5614
5659
  }
5615
5660
  };
5616
- const hide = () => {
5617
- clearTimeout(timeout);
5618
- setVisible(false);
5661
+ const handleWrapperMouseEnter = e => {
5662
+ show(e, true);
5619
5663
  };
5664
+ const hide = (delay = false) => {
5665
+ clearTimeout(showTimeoutRef.current);
5666
+ clearTimeout(hideTimeoutRef.current);
5667
+ if (delay) {
5668
+ hideTimeoutRef.current = setTimeout(() => {
5669
+ setVisible(false);
5670
+ }, hideDelay);
5671
+ } else {
5672
+ setVisible(false);
5673
+ }
5674
+ };
5675
+
5676
+ // Cleanup timeouts on unmount
5677
+ useEffect(() => {
5678
+ return () => {
5679
+ clearTimeout(showTimeoutRef.current);
5680
+ clearTimeout(hideTimeoutRef.current);
5681
+ };
5682
+ }, []);
5683
+
5684
+ // Handle click outside to close tooltip for non-focusable elements
5685
+ useEffect(() => {
5686
+ if (!visible) return;
5687
+ const handleClickOutside = e => {
5688
+ // If clicking outside both the wrapper and tooltip, hide it
5689
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target) && tooltipRef.current && !tooltipRef.current.contains(e.target)) {
5690
+ hide(false);
5691
+ }
5692
+ };
5693
+ document.addEventListener('mousedown', handleClickOutside);
5694
+ return () => {
5695
+ document.removeEventListener('mousedown', handleClickOutside);
5696
+ };
5697
+ }, [visible, hide]);
5698
+ useLayoutEffect(() => {
5699
+ if (!visible || position) {
5700
+ setTooltipPosition(null);
5701
+ setArrowOffset(null);
5702
+ return;
5703
+ }
5704
+ if (!wrapperRef.current || !tooltipRef.current) return;
5705
+ const {
5706
+ tooltipPosition: newPosition,
5707
+ arrowOffset: newArrowOffset
5708
+ } = getTooltipPosition(wrapperRef.current, tooltipRef.current, direction);
5709
+ setTooltipPosition(newPosition);
5710
+ setArrowOffset(newArrowOffset);
5711
+ }, [visible, position]);
5620
5712
  const handleMouseLeave = ({
5621
5713
  relatedTarget
5622
5714
  }) => {
@@ -5624,39 +5716,52 @@ function Tooltip(props) {
5624
5716
  if (relatedTarget === wrapperRef.current || relatedTarget === tooltipRef.current || relatedTarget?.parentElement === tooltipRef.current) {
5625
5717
  return;
5626
5718
  }
5627
- hide();
5719
+ const selection = window.getSelection();
5720
+ if (selection && selection.toString().length > 0) {
5721
+ // Check if selection is within tooltip content
5722
+ const selectionRange = selection.getRangeAt(0);
5723
+ if (tooltipRef.current?.contains(selectionRange.commonAncestorContainer) || tooltipRef.current?.contains(selection.anchorNode) || tooltipRef.current?.contains(selection.focusNode)) {
5724
+ return; // Keep tooltip open during text selection
5725
+ }
5726
+ }
5727
+ hide(true);
5728
+ };
5729
+ const handleTooltipMouseEnter = () => {
5730
+ clearTimeout(hideTimeoutRef.current);
5628
5731
  };
5629
5732
  const handleFocusOut = e => {
5630
5733
  const {
5631
- target
5734
+ relatedTarget
5632
5735
  } = e;
5633
5736
 
5634
- // Don't hide the tooltip if the wrapper or the tooltip itself is clicked.
5635
- const isHovered = target.matches(':hover') || tooltipRef.current?.matches(':hover');
5636
- if (target === wrapperRef.current && isHovered) {
5637
- e.stopPropagation();
5737
+ // Don't hide if focus moved to the tooltip or another element within the wrapper
5738
+ if (tooltipRef.current?.contains(relatedTarget) || wrapperRef.current?.contains(relatedTarget)) {
5638
5739
  return;
5639
5740
  }
5640
- hide();
5741
+ hide(false);
5641
5742
  };
5642
5743
  const hideTooltipViaEscape = e => {
5643
- e.code === 'Escape' && hide();
5744
+ e.code === 'Escape' && hide(false);
5644
5745
  };
5645
5746
  const renderTooltip = () => {
5747
+ const tooltipStyle = position || (tooltipPosition ? `right: ${tooltipPosition.right}; top: ${tooltipPosition.top}px;` : undefined);
5748
+ const arrowStyle = arrowOffset != null ? `margin-top: ${arrowOffset}px;` : undefined;
5646
5749
  return jsxs("div", {
5647
5750
  class: `bio-properties-panel-tooltip ${direction}`,
5648
5751
  role: "tooltip",
5649
5752
  id: "bio-properties-panel-tooltip",
5650
5753
  "aria-labelledby": forId,
5651
- style: position || getTooltipPosition(wrapperRef.current),
5754
+ style: tooltipStyle,
5652
5755
  ref: tooltipRef,
5653
5756
  onClick: e => e.stopPropagation(),
5757
+ onMouseEnter: handleTooltipMouseEnter,
5654
5758
  onMouseLeave: handleMouseLeave,
5655
5759
  children: [jsx("div", {
5656
5760
  class: "bio-properties-panel-tooltip-content",
5657
5761
  children: value
5658
5762
  }), jsx("div", {
5659
- class: "bio-properties-panel-tooltip-arrow"
5763
+ class: "bio-properties-panel-tooltip-arrow",
5764
+ style: arrowStyle
5660
5765
  })]
5661
5766
  });
5662
5767
  };
@@ -5664,7 +5769,7 @@ function Tooltip(props) {
5664
5769
  class: "bio-properties-panel-tooltip-wrapper",
5665
5770
  tabIndex: "0",
5666
5771
  ref: wrapperRef,
5667
- onMouseEnter: e => show(e, true),
5772
+ onMouseEnter: handleWrapperMouseEnter,
5668
5773
  onMouseLeave: handleMouseLeave,
5669
5774
  onFocus: show,
5670
5775
  onBlur: handleFocusOut,
@@ -5675,11 +5780,47 @@ function Tooltip(props) {
5675
5780
 
5676
5781
  // helper
5677
5782
 
5678
- function getTooltipPosition(refElement) {
5783
+ function getTooltipPosition(refElement, tooltipElement, direction) {
5784
+ if (!refElement) {
5785
+ return {
5786
+ tooltipPosition: null,
5787
+ arrowOffset: null
5788
+ };
5789
+ }
5679
5790
  const refPosition = refElement.getBoundingClientRect();
5680
5791
  const right = `calc(100% - ${refPosition.x}px)`;
5681
- const top = `${refPosition.top - 10}px`;
5682
- return `right: ${right}; top: ${top};`;
5792
+ let top = refPosition.top - 10;
5793
+ let arrowOffset = null;
5794
+
5795
+ // Ensure that the tooltip is within the viewport, adjust the top position if needed.
5796
+ // This is only relevant for the 'right' direction for now
5797
+ if (tooltipElement && direction === 'right') {
5798
+ const tooltipRect = tooltipElement.getBoundingClientRect();
5799
+ const viewportHeight = window.innerHeight;
5800
+ const minTop = 0;
5801
+ const maxTop = viewportHeight - tooltipRect.height;
5802
+ const originalTop = top;
5803
+ if (top > maxTop) {
5804
+ top = maxTop;
5805
+ }
5806
+ if (top < minTop) {
5807
+ top = minTop;
5808
+ }
5809
+
5810
+ // Adjust the arrow position if the tooltip had to be moved to stay within viewport
5811
+ if (top !== originalTop) {
5812
+ const defaultMarginTop = 16;
5813
+ const topDiff = top - originalTop;
5814
+ arrowOffset = defaultMarginTop - topDiff;
5815
+ }
5816
+ }
5817
+ return {
5818
+ tooltipPosition: {
5819
+ right,
5820
+ top
5821
+ },
5822
+ arrowOffset
5823
+ };
5683
5824
  }
5684
5825
 
5685
5826
  /**
@@ -5917,12 +6058,17 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
5917
6058
  * The `callback` reference is static and can be safely used in external
5918
6059
  * libraries or as a prop that does not cause rerendering of children.
5919
6060
  *
6061
+ * The ref update is deferred to useLayoutEffect to prevent stale-closure
6062
+ * bugs when Chrome fires blur on elements removed during re-render.
6063
+ *
5920
6064
  * @param {Function} callback function with changing reference
5921
6065
  * @returns {Function} static function reference
5922
6066
  */
5923
6067
  function useStaticCallback(callback) {
5924
6068
  const callbackRef = useRef(callback);
5925
- callbackRef.current = callback;
6069
+ useLayoutEffect(() => {
6070
+ callbackRef.current = callback;
6071
+ });
5926
6072
  return useCallback((...args) => callbackRef.current(...args), []);
5927
6073
  }
5928
6074
  function useElementVisible(element) {
@@ -5942,6 +6088,10 @@ function useElementVisible(element) {
5942
6088
  }, [element, visible]);
5943
6089
  return visible;
5944
6090
  }
6091
+
6092
+ /**
6093
+ * @param {import('../PropertiesPanel').GroupDefinition} props
6094
+ */
5945
6095
  function Group(props) {
5946
6096
  const {
5947
6097
  element,
@@ -5996,8 +6146,6 @@ function Group(props) {
5996
6146
  class: classnames('bio-properties-panel-group-header', edited ? '' : 'empty', open ? 'open' : '', sticky && open ? 'sticky' : ''),
5997
6147
  onClick: toggleOpen,
5998
6148
  children: [jsx("div", {
5999
- title: props.tooltip ? null : label,
6000
- "data-title": label,
6001
6149
  class: "bio-properties-panel-group-header-title",
6002
6150
  children: jsx(TooltipWrapper, {
6003
6151
  value: props.tooltip,
@@ -6201,9 +6349,11 @@ function PropertiesPanel$1(props) {
6201
6349
  return get(layout, key, defaultValue);
6202
6350
  };
6203
6351
  const setLayoutForKey = (key, config) => {
6204
- const newLayout = assign({}, layout);
6205
- set$1(newLayout, key, config);
6206
- setLayout(newLayout);
6352
+ setLayout(prevLayout => {
6353
+ const newLayout = assign({}, prevLayout);
6354
+ set$1(newLayout, key, config);
6355
+ return newLayout;
6356
+ });
6207
6357
  };
6208
6358
  const layoutContext = {
6209
6359
  layout,
@@ -6402,7 +6552,6 @@ function CollapsibleEntry(props) {
6402
6552
  class: "bio-properties-panel-collapsible-entry-header",
6403
6553
  onClick: toggleOpen,
6404
6554
  children: [jsx("div", {
6405
- title: label || placeholderLabel,
6406
6555
  class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
6407
6556
  children: label || placeholderLabel
6408
6557
  }), jsx("button", {
@@ -6438,6 +6587,10 @@ function CollapsibleEntry(props) {
6438
6587
  })]
6439
6588
  });
6440
6589
  }
6590
+
6591
+ /**
6592
+ * @param {import('../PropertiesPanel').ListItemDefinition} props
6593
+ */
6441
6594
  function ListItem(props) {
6442
6595
  const {
6443
6596
  autoFocusEntry,
@@ -6539,8 +6692,6 @@ function ListGroup(props) {
6539
6692
  class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
6540
6693
  onClick: hasItems ? toggleOpen : noop$6,
6541
6694
  children: [jsx("div", {
6542
- title: props.tooltip ? null : label,
6543
- "data-title": label,
6544
6695
  class: "bio-properties-panel-group-header-title",
6545
6696
  children: jsx(TooltipWrapper, {
6546
6697
  value: props.tooltip,
@@ -6609,6 +6760,13 @@ function getNewItemIds(newItems, oldItems) {
6609
6760
  const oldIds = oldItems.map(item => item.id);
6610
6761
  return newIds.filter(itemId => !oldIds.includes(itemId));
6611
6762
  }
6763
+
6764
+ /**
6765
+ * @param {Object} props
6766
+ * @param {Object} props.element
6767
+ * @param {String} props.forId - id of the entry the description is used for
6768
+ * @param {String} props.value
6769
+ */
6612
6770
  function Description$1(props) {
6613
6771
  const {
6614
6772
  element,
@@ -6738,6 +6896,16 @@ function isEdited$8(node) {
6738
6896
  function prefixId$8(id) {
6739
6897
  return `bio-properties-panel-${id}`;
6740
6898
  }
6899
+
6900
+ /**
6901
+ * Button to open popups.
6902
+ *
6903
+ * @param {Object} props
6904
+ * @param {Function} props.onClick - Callback to trigger when the button is clicked.
6905
+ * @param {string} [props.title] - Tooltip text for the button.
6906
+ * @param {boolean} [props.disabled] - Whether the button is disabled.
6907
+ * @param {string} [props.className] - Additional class names for the button.
6908
+ */
6741
6909
  function OpenPopupButton({
6742
6910
  onClick,
6743
6911
  title = 'Open pop-up editor'
@@ -6881,6 +7049,7 @@ const FeelEditor = forwardRef((props, ref) => {
6881
7049
  enableGutters,
6882
7050
  value,
6883
7051
  onInput,
7052
+ onKeyDown: onKeyDownProp = noop$4,
6884
7053
  onFeelToggle = noop$4,
6885
7054
  onLint = noop$4,
6886
7055
  onOpenPopup = noop$4,
@@ -6913,6 +7082,8 @@ const FeelEditor = forwardRef((props, ref) => {
6913
7082
  * - AND the cursor is at the beginning of the input
6914
7083
  */
6915
7084
  const onKeyDown = e => {
7085
+ // Call parent onKeyDown handler first
7086
+ onKeyDownProp(e);
6916
7087
  if (e.key !== 'Backspace' || !editor) {
6917
7088
  return;
6918
7089
  }
@@ -7029,6 +7200,22 @@ function FeelIcon(props) {
7029
7200
  children: jsx(FeelIcon$1, {})
7030
7201
  });
7031
7202
  }
7203
+
7204
+ /**
7205
+ * @param {KeyboardEvent} event
7206
+ * @return {boolean}
7207
+ */
7208
+ function isCmd(event) {
7209
+ // ensure we don't react to AltGr
7210
+ // (mapped to CTRL + ALT)
7211
+ if (event.altKey) {
7212
+ return false;
7213
+ }
7214
+ return event.ctrlKey || event.metaKey;
7215
+ }
7216
+ function isCmdWithChar(event) {
7217
+ return isCmd(event) && event.key.length === 1 && /^[a-zA-Z]$/.test(event.key);
7218
+ }
7032
7219
  function ToggleSwitch(props) {
7033
7220
  const {
7034
7221
  id,
@@ -7137,7 +7324,7 @@ function ToggleSwitchEntry(props) {
7137
7324
  inline: inline,
7138
7325
  tooltip: tooltip,
7139
7326
  element: element
7140
- }), jsx(Description$1, {
7327
+ }, element), jsx(Description$1, {
7141
7328
  forId: id,
7142
7329
  element: element,
7143
7330
  value: description
@@ -7310,7 +7497,7 @@ function prefixId$6(id) {
7310
7497
  const noop$2 = () => {};
7311
7498
 
7312
7499
  /**
7313
- * @typedef {'required'|'optional'|'static'} FeelType
7500
+ * @typedef {'required'|'optional'|'optional-default-enabled'|'static'} FeelType
7314
7501
  */
7315
7502
 
7316
7503
  /**
@@ -7340,7 +7527,7 @@ function FeelTextfield(props) {
7340
7527
  element,
7341
7528
  label,
7342
7529
  hostLanguage,
7343
- onInput,
7530
+ onInput: commitValue,
7344
7531
  onBlur,
7345
7532
  onError,
7346
7533
  placeholder,
@@ -7353,11 +7540,17 @@ function FeelTextfield(props) {
7353
7540
  OptionalComponent = OptionalFeelInput,
7354
7541
  tooltip
7355
7542
  } = props;
7356
- const [localValue, setLocalValue] = useState(value);
7543
+ const [localValue, setLocalValue] = useState(getInitialFeelLocalValue(feel, value));
7357
7544
  const editorRef = useShowEntryEvent(id);
7358
7545
  const containerRef = useRef();
7359
- const feelActive = isString(localValue) && localValue.startsWith('=') || feel === 'required';
7360
- const feelOnlyValue = isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
7546
+ const onInput = useCallback(newValue => {
7547
+ // we don't commit empty FEEL expressions,
7548
+ // but instead serialize them as <undefined>
7549
+ const newModelValue = newValue === '' || newValue === '=' ? undefined : newValue;
7550
+ commitValue(newModelValue);
7551
+ }, [commitValue]);
7552
+ const feelActive = isFeelActive(feel, localValue);
7553
+ const feelOnlyValue = getFeelValue(localValue);
7361
7554
  const feelLanguageContext = useContext(FeelLanguageContext);
7362
7555
  const [focus, _setFocus] = useState(undefined);
7363
7556
  const {
@@ -7375,13 +7568,7 @@ function FeelTextfield(props) {
7375
7568
  /**
7376
7569
  * @type { import('min-dash').DebouncedFunction }
7377
7570
  */
7378
- const handleInputCallback = useDebounce(onInput, debounce);
7379
- const handleInput = newValue => {
7380
- // we don't commit empty FEEL expressions,
7381
- // but instead serialize them as <undefined>
7382
- const newModelValue = newValue === '' || newValue === '=' ? undefined : newValue;
7383
- handleInputCallback(newModelValue);
7384
- };
7571
+ const handleInput = useDebounce(onInput, debounce);
7385
7572
  const handleFeelToggle = useStaticCallback(() => {
7386
7573
  if (feel === 'required') {
7387
7574
  return;
@@ -7394,7 +7581,7 @@ function FeelTextfield(props) {
7394
7581
  handleInput(feelOnlyValue);
7395
7582
  }
7396
7583
  });
7397
- const handleLocalInput = newValue => {
7584
+ const handleLocalInput = (newValue, useDebounce = true) => {
7398
7585
  if (feelActive) {
7399
7586
  newValue = '=' + newValue;
7400
7587
  }
@@ -7402,23 +7589,33 @@ function FeelTextfield(props) {
7402
7589
  return;
7403
7590
  }
7404
7591
  setLocalValue(newValue);
7405
- handleInput(newValue);
7592
+ if (useDebounce) {
7593
+ handleInput(newValue);
7594
+ } else {
7595
+ onInput(newValue);
7596
+ }
7406
7597
  if (!feelActive && isString(newValue) && newValue.startsWith('=')) {
7407
7598
  // focus is behind `=` sign that will be removed
7408
7599
  setFocus(-1);
7409
7600
  }
7410
7601
  };
7411
7602
  const handleOnBlur = e => {
7603
+ handleInput.cancel?.();
7412
7604
  if (e.target.type === 'checkbox') {
7413
7605
  onInput(e.target.checked);
7414
7606
  } else {
7415
7607
  const trimmedValue = e.target.value.trim();
7416
- onInput(trimmedValue);
7608
+ handleLocalInput(trimmedValue, false);
7417
7609
  }
7418
7610
  if (onBlur) {
7419
7611
  onBlur(e);
7420
7612
  }
7421
7613
  };
7614
+ const handleOnKeyDown = e => {
7615
+ if (isCmdWithChar(e)) {
7616
+ handleInput.flush?.();
7617
+ }
7618
+ };
7422
7619
  const handleLint = useStaticCallback((lint = []) => {
7423
7620
  const syntaxError = lint.some(report => report.type === 'Syntax Error');
7424
7621
  if (syntaxError) {
@@ -7485,12 +7682,26 @@ function FeelTextfield(props) {
7485
7682
  if (feelActive || isPopupOpen) {
7486
7683
  return;
7487
7684
  }
7488
- const data = event.clipboardData.getData('application/FEEL');
7489
- if (data) {
7685
+ const feelData = event.clipboardData.getData('application/FEEL');
7686
+ if (feelData) {
7490
7687
  setTimeout(() => {
7491
7688
  handleFeelToggle();
7492
7689
  setFocus();
7493
7690
  });
7691
+ return;
7692
+ }
7693
+ const input = event.target;
7694
+ const isFieldEmpty = !input.value;
7695
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
7696
+ if (isFieldEmpty || isAllSelected) {
7697
+ const textData = event.clipboardData.getData('text');
7698
+ const trimmedValue = textData.trim();
7699
+ setLocalValue(trimmedValue);
7700
+ handleInput(trimmedValue);
7701
+ if (!feelActive && isString(trimmedValue) && trimmedValue.startsWith('=')) {
7702
+ setFocus(trimmedValue.length - 1);
7703
+ }
7704
+ event.preventDefault();
7494
7705
  }
7495
7706
  };
7496
7707
  containerRef.current.addEventListener('copy', copyHandler);
@@ -7526,11 +7737,12 @@ function FeelTextfield(props) {
7526
7737
  ref: containerRef,
7527
7738
  children: [jsx(FeelIndicator, {
7528
7739
  active: feelActive,
7529
- disabled: feel !== 'optional' || disabled,
7740
+ disabled: !isFeelOptional(feel) || disabled,
7530
7741
  onClick: handleFeelToggle
7531
7742
  }), feelActive ? jsx(FeelEditor, {
7532
7743
  name: id,
7533
7744
  onInput: handleLocalInput,
7745
+ onKeyDown: handleOnKeyDown,
7534
7746
  contentAttributes: {
7535
7747
  'id': prefixId$5(id),
7536
7748
  'aria-label': label
@@ -7553,6 +7765,7 @@ function FeelTextfield(props) {
7553
7765
  ...props,
7554
7766
  popupOpen: isPopupOpen,
7555
7767
  onInput: handleLocalInput,
7768
+ onKeyDown: handleOnKeyDown,
7556
7769
  onBlur: handleOnBlur,
7557
7770
  contentAttributes: {
7558
7771
  'id': prefixId$5(id),
@@ -7571,6 +7784,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
7571
7784
  id,
7572
7785
  disabled,
7573
7786
  onInput,
7787
+ onKeyDown,
7574
7788
  value,
7575
7789
  onFocus,
7576
7790
  onBlur,
@@ -7606,6 +7820,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
7606
7820
  class: "bio-properties-panel-input",
7607
7821
  onInput: e => onInput(e.target.value),
7608
7822
  onFocus: onFocus,
7823
+ onKeyDown: onKeyDown,
7609
7824
  onBlur: onBlur,
7610
7825
  placeholder: placeholder,
7611
7826
  value: value || ''
@@ -7979,6 +8194,87 @@ function isEdited$5(node) {
7979
8194
  function prefixId$5(id) {
7980
8195
  return `bio-properties-panel-${id}`;
7981
8196
  }
8197
+
8198
+ /**
8199
+ * Determine if FEEL is optional for the configured {@link FeelType}.
8200
+ *
8201
+ * @param {FeelType} feelType
8202
+ *
8203
+ * @return {boolean}
8204
+ */
8205
+ function isFeelOptional(feelType) {
8206
+ return feelType === 'optional' || feelType === 'optional-default-enabled';
8207
+ }
8208
+
8209
+ /**
8210
+ * Determine if FEEL editing is currently active.
8211
+ *
8212
+ * @param {FeelType} feelType
8213
+ * @param {string} localValue
8214
+ *
8215
+ * @return {boolean}
8216
+ */
8217
+ function isFeelActive(feelType, localValue) {
8218
+ if (feelType === 'required') {
8219
+ return true;
8220
+ }
8221
+ if (isString(localValue)) {
8222
+ if (localValue.startsWith('=')) {
8223
+ return true;
8224
+ }
8225
+ }
8226
+ return false;
8227
+ }
8228
+
8229
+ /**
8230
+ * @template T
8231
+ * @param {T} value
8232
+ *
8233
+ * @return {string|T}
8234
+ */
8235
+ function getFeelValue(value) {
8236
+ if (isString(value) && value.startsWith('=')) {
8237
+ return value.substring(1);
8238
+ }
8239
+ return value;
8240
+ }
8241
+
8242
+ /**
8243
+ * Initialize local FEEL value.
8244
+ *
8245
+ * `optional-default-enabled` starts in FEEL mode if no value or empty string is provided.
8246
+ *
8247
+ * @template T
8248
+ * @param {FeelType} feelType
8249
+ * @param {T} value
8250
+ *
8251
+ * @return {string|T}
8252
+ */
8253
+ function getInitialFeelLocalValue(feelType, value) {
8254
+ if (feelType === 'optional-default-enabled' && (value === undefined || value === '')) {
8255
+ return '=';
8256
+ }
8257
+ return value;
8258
+ }
8259
+
8260
+ /**
8261
+ * @typedef { { value: string, label: string, disabled: boolean, children: { value: string, label: string, disabled: boolean } } } Option
8262
+ */
8263
+
8264
+ /**
8265
+ * Provides basic select input.
8266
+ *
8267
+ * @param {object} props
8268
+ * @param {string} props.id
8269
+ * @param {string[]} props.path
8270
+ * @param {string} props.label
8271
+ * @param {Function} props.onChange
8272
+ * @param {Function} props.onFocus
8273
+ * @param {Function} props.onBlur
8274
+ * @param {Array<Option>} [props.options]
8275
+ * @param {string} props.value
8276
+ * @param {boolean} [props.disabled]
8277
+ */
7982
8278
  function Select(props) {
7983
8279
  const {
7984
8280
  id,
@@ -8144,12 +8440,13 @@ function TextArea(props) {
8144
8440
  id,
8145
8441
  label,
8146
8442
  debounce,
8147
- onInput,
8443
+ onInput: commitValue,
8148
8444
  value = '',
8149
8445
  disabled,
8150
8446
  monospace,
8151
8447
  onFocus,
8152
8448
  onBlur,
8449
+ onPaste,
8153
8450
  autoResize = true,
8154
8451
  placeholder,
8155
8452
  rows = autoResize ? 1 : 2,
@@ -8157,16 +8454,16 @@ function TextArea(props) {
8157
8454
  } = props;
8158
8455
  const [localValue, setLocalValue] = useState(value);
8159
8456
  const ref = useShowEntryEvent(id);
8457
+ const onInput = useCallback(newValue => {
8458
+ const newModelValue = newValue === '' ? undefined : newValue;
8459
+ commitValue(newModelValue);
8460
+ }, [commitValue]);
8160
8461
  const visible = useElementVisible(ref.current);
8161
8462
 
8162
8463
  /**
8163
8464
  * @type { import('min-dash').DebouncedFunction }
8164
8465
  */
8165
- const handleInputCallback = useDebounce(onInput, debounce);
8166
- const handleInput = newValue => {
8167
- const newModelValue = newValue === '' ? undefined : newValue;
8168
- handleInputCallback(newModelValue);
8169
- };
8466
+ const handleInput = useDebounce(onInput, debounce);
8170
8467
  const handleLocalInput = e => {
8171
8468
  autoResize && resizeToContents(e.target);
8172
8469
  if (e.target.value === localValue) {
@@ -8179,11 +8476,40 @@ function TextArea(props) {
8179
8476
  const trimmedValue = e.target.value.trim();
8180
8477
 
8181
8478
  // trim and commit on blur
8479
+ handleInput.cancel?.();
8182
8480
  onInput(trimmedValue);
8481
+ setLocalValue(trimmedValue);
8183
8482
  if (onBlur) {
8184
8483
  onBlur(e);
8185
8484
  }
8186
8485
  };
8486
+ const handleOnPaste = e => {
8487
+ const input = e.target;
8488
+ const isFieldEmpty = !input.value;
8489
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
8490
+
8491
+ // Trim and handle paste if field is empty or all content is selected
8492
+ if (isFieldEmpty || isAllSelected) {
8493
+ const trimmedValue = e.clipboardData.getData('text').trim();
8494
+ setLocalValue(trimmedValue);
8495
+ handleInput(trimmedValue);
8496
+ if (onPaste) {
8497
+ onPaste(e);
8498
+ }
8499
+ e.preventDefault();
8500
+ return;
8501
+ }
8502
+
8503
+ // Allow default paste behavior for normal text editing
8504
+ if (onPaste) {
8505
+ onPaste(e);
8506
+ }
8507
+ };
8508
+ const handleOnKeyDown = e => {
8509
+ if (isCmdWithChar(e)) {
8510
+ handleInput.flush?.();
8511
+ }
8512
+ };
8187
8513
  useLayoutEffect(() => {
8188
8514
  autoResize && resizeToContents(ref.current);
8189
8515
  }, []);
@@ -8215,7 +8541,9 @@ function TextArea(props) {
8215
8541
  class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
8216
8542
  onInput: handleLocalInput,
8217
8543
  onFocus: onFocus,
8544
+ onKeyDown: handleOnKeyDown,
8218
8545
  onBlur: handleOnBlur,
8546
+ onPaste: handleOnPaste,
8219
8547
  placeholder: placeholder,
8220
8548
  rows: rows,
8221
8549
  value: localValue,
@@ -8236,6 +8564,7 @@ function TextArea(props) {
8236
8564
  * @param {Function} props.setValue
8237
8565
  * @param {Function} props.onFocus
8238
8566
  * @param {Function} props.onBlur
8567
+ * @param {Function} props.onPaste
8239
8568
  * @param {number} props.rows
8240
8569
  * @param {boolean} props.monospace
8241
8570
  * @param {Function} [props.validate]
@@ -8256,6 +8585,7 @@ function TextAreaEntry(props) {
8256
8585
  validate,
8257
8586
  onFocus,
8258
8587
  onBlur,
8588
+ onPaste,
8259
8589
  placeholder,
8260
8590
  autoResize,
8261
8591
  tooltip
@@ -8291,6 +8621,7 @@ function TextAreaEntry(props) {
8291
8621
  onInput: onInput,
8292
8622
  onFocus: onFocus,
8293
8623
  onBlur: onBlur,
8624
+ onPaste: onPaste,
8294
8625
  rows: rows,
8295
8626
  debounce: debounce,
8296
8627
  monospace: monospace,
@@ -8324,32 +8655,57 @@ function Textfield(props) {
8324
8655
  disabled = false,
8325
8656
  id,
8326
8657
  label,
8327
- onInput,
8658
+ onInput: commitValue,
8328
8659
  onFocus,
8329
8660
  onBlur,
8661
+ onPaste,
8330
8662
  placeholder,
8331
8663
  value = '',
8332
8664
  tooltip
8333
8665
  } = props;
8334
8666
  const [localValue, setLocalValue] = useState(value || '');
8335
8667
  const ref = useShowEntryEvent(id);
8668
+ const onInput = useCallback(newValue => {
8669
+ const newModelValue = newValue === '' ? undefined : newValue;
8670
+ commitValue(newModelValue);
8671
+ }, [commitValue]);
8336
8672
 
8337
8673
  /**
8338
8674
  * @type { import('min-dash').DebouncedFunction }
8339
8675
  */
8340
- const handleInputCallback = useDebounce(onInput, debounce);
8676
+ const handleInput = useDebounce(onInput, debounce);
8341
8677
  const handleOnBlur = e => {
8342
8678
  const trimmedValue = e.target.value.trim();
8343
8679
 
8344
8680
  // trim and commit on blur
8681
+ handleInput.cancel?.();
8345
8682
  onInput(trimmedValue);
8683
+ setLocalValue(trimmedValue);
8346
8684
  if (onBlur) {
8347
8685
  onBlur(e);
8348
8686
  }
8349
8687
  };
8350
- const handleInput = newValue => {
8351
- const newModelValue = newValue === '' ? undefined : newValue;
8352
- handleInputCallback(newModelValue);
8688
+ const handleOnPaste = e => {
8689
+ const input = e.target;
8690
+ const isFieldEmpty = !input.value;
8691
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
8692
+
8693
+ // Trim and handle paste if field is empty or all content is selected (overwrite)
8694
+ if (isFieldEmpty || isAllSelected) {
8695
+ const trimmedValue = e.clipboardData.getData('text').trim();
8696
+ setLocalValue(trimmedValue);
8697
+ handleInput(trimmedValue);
8698
+ if (onPaste) {
8699
+ onPaste(e);
8700
+ }
8701
+ e.preventDefault();
8702
+ return;
8703
+ }
8704
+
8705
+ // Allow default paste behavior for normal text editing
8706
+ if (onPaste) {
8707
+ onPaste(e);
8708
+ }
8353
8709
  };
8354
8710
  const handleLocalInput = e => {
8355
8711
  if (e.target.value === localValue) {
@@ -8364,6 +8720,11 @@ function Textfield(props) {
8364
8720
  }
8365
8721
  setLocalValue(value);
8366
8722
  }, [value]);
8723
+ const handleOnKeyDown = e => {
8724
+ if (isCmdWithChar(e)) {
8725
+ handleInput.flush?.();
8726
+ }
8727
+ };
8367
8728
  return jsxs("div", {
8368
8729
  class: "bio-properties-panel-textfield",
8369
8730
  children: [jsx("label", {
@@ -8386,7 +8747,9 @@ function Textfield(props) {
8386
8747
  class: "bio-properties-panel-input",
8387
8748
  onInput: handleLocalInput,
8388
8749
  onFocus: onFocus,
8750
+ onKeyDown: handleOnKeyDown,
8389
8751
  onBlur: handleOnBlur,
8752
+ onPaste: handleOnPaste,
8390
8753
  placeholder: placeholder,
8391
8754
  value: localValue
8392
8755
  })]
@@ -8421,6 +8784,7 @@ function TextfieldEntry(props) {
8421
8784
  validate,
8422
8785
  onFocus,
8423
8786
  onBlur,
8787
+ onPaste,
8424
8788
  placeholder,
8425
8789
  tooltip
8426
8790
  } = props;
@@ -8456,6 +8820,7 @@ function TextfieldEntry(props) {
8456
8820
  onInput: onInput,
8457
8821
  onFocus: onFocus,
8458
8822
  onBlur: onBlur,
8823
+ onPaste: onPaste,
8459
8824
  placeholder: placeholder,
8460
8825
  value: value,
8461
8826
  tooltip: tooltip,
@@ -8725,6 +9090,7 @@ function Title(props) {
8725
9090
  class: "bio-properties-panel-popup__title",
8726
9091
  children: title
8727
9092
  }), children, showCloseButton && jsx("button", {
9093
+ type: "button",
8728
9094
  title: closeButtonTooltip,
8729
9095
  class: "bio-properties-panel-popup__close",
8730
9096
  onClick: onClose,
@@ -8766,6 +9132,25 @@ function cancel(event) {
8766
9132
  event.preventDefault();
8767
9133
  event.stopPropagation();
8768
9134
  }
9135
+
9136
+ /**
9137
+ * @typedef {Object} FeelPopupProps
9138
+ * @property {string} entryId
9139
+ * @property {Function} onInput
9140
+ * @property {Function} onClose
9141
+ * @property {string} title
9142
+ * @property {'feel'|'feelers'} type
9143
+ * @property {string} value
9144
+ * @property {Array} [links]
9145
+ * @property {Array|Object} [variables]
9146
+ * @property {Object} [position]
9147
+ * @property {string} [hostLanguage]
9148
+ * @property {boolean} [singleLine]
9149
+ * @property {HTMLElement} [sourceElement]
9150
+ * @property {HTMLElement|string} [tooltipContainer]
9151
+ * @property {Object} [eventBus]
9152
+ */
9153
+
8769
9154
  const FEEL_POPUP_WIDTH = 700;
8770
9155
  const FEEL_POPUP_HEIGHT = 250;
8771
9156