@bpmn-io/form-js-editor 1.18.0 → 1.19.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.
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import Ids from 'ids';
1
+ import { Ids } from 'ids';
2
2
  import { FormFieldRegistry as FormFieldRegistry$1, iconsByType, Label as Label$3, IFrame, Text as Text$1, Html, Table, ExpressionField, DocumentPreview, FormFields, sanitizeImageSource, getAncestryList, FormContext, FormRenderContext, FormComponent, getScrollContainer, FieldFactory, FormLayouter, PathRegistry, Importer, FeelExpressionLanguage, OPTIONS_SOURCES, OPTIONS_SOURCES_PATHS, clone, runRecursively, getSchemaVariables, DATETIME_SUBTYPES, DATE_LABEL_PATH, TIME_LABEL_PATH, TEXT_VIEW_DEFAULT_TEXT, TIME_USE24H_PATH, DATETIME_SUBTYPE_PATH, DATETIME_SUBTYPES_LABELS, TIME_INTERVAL_PATH, TIME_SERIALISING_FORMAT_PATH, DATE_DISALLOW_PAST_PATH, TIME_SERIALISING_FORMATS, TIME_SERIALISINGFORMAT_LABELS, getOptionsSource, OPTIONS_SOURCES_DEFAULTS, OPTIONS_SOURCES_LABELS, SECURITY_ATTRIBUTES_DEFINITIONS, createFormContainer, createInjector, MarkdownRendererModule, schemaVersion } from '@bpmn-io/form-js-viewer';
3
3
  export { schemaVersion } from '@bpmn-io/form-js-viewer';
4
4
  import { isArray, isFunction, isNumber, bind, assign, debounce, forEach, isString, uniqueBy, isObject, get, isDefined, set as set$1, reduce, isNil, without, has } from 'min-dash';
@@ -13,7 +13,7 @@ import { classes, query, event, domify } from 'min-dom';
13
13
  import { arrayMoveMutable } from 'array-move';
14
14
  import { FeelersEditor } from 'feelers';
15
15
  import Editor from '@bpmn-io/feel-editor';
16
- import { lineNumbers, EditorView } from '@codemirror/view';
16
+ import { EditorView, lineNumbers } from '@codemirror/view';
17
17
  import * as focusTrap from 'focus-trap';
18
18
  import Big from 'big.js';
19
19
 
@@ -2870,11 +2870,35 @@ EditorActions.prototype._registerDefaultActions = function (injector) {
2870
2870
  }
2871
2871
  });
2872
2872
  }
2873
+ if (copyPaste && selection) {
2874
+ this.register('duplicate', function () {
2875
+ var selectedElements = selection.get();
2876
+ if (selectedElements.length) {
2877
+ return copyPaste.duplicate(selectedElements);
2878
+ }
2879
+ });
2880
+ }
2873
2881
  if (copyPaste) {
2874
2882
  this.register('paste', function () {
2875
2883
  copyPaste.paste();
2876
2884
  });
2877
2885
  }
2886
+ if (copyPaste && selection && rules) {
2887
+ this.register('cut', function () {
2888
+ var selectedElements = selection.get();
2889
+ if (!selectedElements.length) {
2890
+ return;
2891
+ }
2892
+ var allowed = rules.allowed('elements.delete', {
2893
+ elements: selectedElements
2894
+ });
2895
+ if (allowed === false) {
2896
+ return;
2897
+ }
2898
+ var cuttableElements = isArray(allowed) ? allowed : selectedElements;
2899
+ return copyPaste.cut(cuttableElements.slice());
2900
+ });
2901
+ }
2878
2902
  if (zoomScroll) {
2879
2903
  this.register('stepZoom', function (opts) {
2880
2904
  zoomScroll.stepZoom(opts.value);
@@ -3093,6 +3117,8 @@ const EditorExpressionLanguageModule = {
3093
3117
 
3094
3118
  var KEYS_COPY = ['c', 'C'];
3095
3119
  var KEYS_PASTE = ['v', 'V'];
3120
+ var KEYS_DUPLICATE = ['d', 'D'];
3121
+ var KEYS_CUT = ['x', 'X'];
3096
3122
  var KEYS_REDO = ['y', 'Y'];
3097
3123
  var KEYS_UNDO = ['z', 'Z'];
3098
3124
 
@@ -3108,7 +3134,7 @@ function hasModifier(event) {
3108
3134
  * @param {KeyboardEvent} event
3109
3135
  * @return {boolean}
3110
3136
  */
3111
- function isCmd(event) {
3137
+ function isCmd$1(event) {
3112
3138
  // ensure we don't react to AltGr
3113
3139
  // (mapped to CTRL + ALT)
3114
3140
  if (event.altKey) {
@@ -3140,28 +3166,42 @@ function isShift(event) {
3140
3166
  * @param {KeyboardEvent} event
3141
3167
  */
3142
3168
  function isCopy(event) {
3143
- return isCmd(event) && isKey(KEYS_COPY, event);
3169
+ return isCmd$1(event) && isKey(KEYS_COPY, event);
3144
3170
  }
3145
3171
 
3146
3172
  /**
3147
3173
  * @param {KeyboardEvent} event
3148
3174
  */
3149
3175
  function isPaste(event) {
3150
- return isCmd(event) && isKey(KEYS_PASTE, event);
3176
+ return isCmd$1(event) && isKey(KEYS_PASTE, event);
3177
+ }
3178
+
3179
+ /**
3180
+ * @param {KeyboardEvent} event
3181
+ */
3182
+ function isDuplicate(event) {
3183
+ return isCmd$1(event) && isKey(KEYS_DUPLICATE, event);
3184
+ }
3185
+
3186
+ /**
3187
+ * @param {KeyboardEvent} event
3188
+ */
3189
+ function isCut(event) {
3190
+ return isCmd$1(event) && isKey(KEYS_CUT, event);
3151
3191
  }
3152
3192
 
3153
3193
  /**
3154
3194
  * @param {KeyboardEvent} event
3155
3195
  */
3156
3196
  function isUndo(event) {
3157
- return isCmd(event) && !isShift(event) && isKey(KEYS_UNDO, event);
3197
+ return isCmd$1(event) && !isShift(event) && isKey(KEYS_UNDO, event);
3158
3198
  }
3159
3199
 
3160
3200
  /**
3161
3201
  * @param {KeyboardEvent} event
3162
3202
  */
3163
3203
  function isRedo(event) {
3164
- return isCmd(event) && (isKey(KEYS_REDO, event) || isKey(KEYS_UNDO, event) && isShift(event));
3204
+ return isCmd$1(event) && (isKey(KEYS_REDO, event) || isKey(KEYS_UNDO, event) && isShift(event));
3165
3205
  }
3166
3206
 
3167
3207
  /**
@@ -3330,7 +3370,7 @@ Keyboard.prototype.removeListener = function (listener, type) {
3330
3370
  this._eventBus.off(type || KEYDOWN_EVENT, listener);
3331
3371
  };
3332
3372
  Keyboard.prototype.hasModifier = hasModifier;
3333
- Keyboard.prototype.isCmd = isCmd;
3373
+ Keyboard.prototype.isCmd = isCmd$1;
3334
3374
  Keyboard.prototype.isShift = isShift;
3335
3375
  Keyboard.prototype.isKey = isKey;
3336
3376
 
@@ -3415,6 +3455,26 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3415
3455
  }
3416
3456
  });
3417
3457
 
3458
+ // duplicate
3459
+ // CTRL/CMD + D
3460
+ addListener('duplicate', function (context) {
3461
+ var event = context.keyEvent;
3462
+ if (isDuplicate(event)) {
3463
+ editorActions.trigger('duplicate');
3464
+ return true;
3465
+ }
3466
+ });
3467
+
3468
+ // cut
3469
+ // CTRL/CMD + X
3470
+ addListener('cut', function (context) {
3471
+ var event = context.keyEvent;
3472
+ if (isCut(event)) {
3473
+ editorActions.trigger('cut');
3474
+ return true;
3475
+ }
3476
+ });
3477
+
3418
3478
  // zoom in one step
3419
3479
  // CTRL/CMD + +
3420
3480
  addListener('stepZoom', function (context) {
@@ -3422,7 +3482,7 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3422
3482
 
3423
3483
  // quirk: it has to be triggered by `=` as well to work on international keyboard layout
3424
3484
  // cf: https://github.com/bpmn-io/bpmn-js/issues/1362#issuecomment-722989754
3425
- if (isKey(['+', 'Add', '='], event) && isCmd(event)) {
3485
+ if (isKey(['+', 'Add', '='], event) && isCmd$1(event)) {
3426
3486
  editorActions.trigger('stepZoom', {
3427
3487
  value: 1
3428
3488
  });
@@ -3434,7 +3494,7 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3434
3494
  // CTRL + -
3435
3495
  addListener('stepZoom', function (context) {
3436
3496
  var event = context.keyEvent;
3437
- if (isKey(['-', 'Subtract'], event) && isCmd(event)) {
3497
+ if (isKey(['-', 'Subtract'], event) && isCmd$1(event)) {
3438
3498
  editorActions.trigger('stepZoom', {
3439
3499
  value: -1
3440
3500
  });
@@ -3446,7 +3506,7 @@ KeyboardBindings.prototype.registerBindings = function (keyboard, editorActions)
3446
3506
  // CTRL + 0
3447
3507
  addListener('zoom', function (context) {
3448
3508
  var event = context.keyEvent;
3449
- if (isKey('0', event) && isCmd(event)) {
3509
+ if (isKey('0', event) && isCmd$1(event)) {
3450
3510
  editorActions.trigger('zoom', {
3451
3511
  value: 1
3452
3512
  });
@@ -5453,6 +5513,21 @@ OpenPopupIcon.defaultProps = {
5453
5513
  xmlns: "http://www.w3.org/2000/svg",
5454
5514
  viewBox: "0 0 16 16"
5455
5515
  };
5516
+
5517
+ /**
5518
+ * @typedef { {
5519
+ * getElementLabel: (element: object) => string,
5520
+ * getTypeLabel: (element: object) => string,
5521
+ * getElementIcon: (element: object) => import('preact').Component,
5522
+ * getDocumentationRef: (element: object) => string
5523
+ * } } HeaderProvider
5524
+ */
5525
+
5526
+ /**
5527
+ * @param {Object} props
5528
+ * @param {Object} props.element,
5529
+ * @param {HeaderProvider} props.headerProvider
5530
+ */
5456
5531
  function Header(props) {
5457
5532
  const {
5458
5533
  element,
@@ -5480,11 +5555,9 @@ function Header(props) {
5480
5555
  }), jsxs("div", {
5481
5556
  class: "bio-properties-panel-header-labels",
5482
5557
  children: [jsx("div", {
5483
- title: type,
5484
5558
  class: "bio-properties-panel-header-type",
5485
5559
  children: type
5486
5560
  }), label ? jsx("div", {
5487
- title: label,
5488
5561
  class: "bio-properties-panel-header-label",
5489
5562
  children: label
5490
5563
  }) : null]
@@ -5572,6 +5645,27 @@ function useTooltipContext(id, element) {
5572
5645
  } = useContext(TooltipContext);
5573
5646
  return getTooltipForId(id, element);
5574
5647
  }
5648
+
5649
+ /**
5650
+ * @typedef {Object} TooltipProps
5651
+ * @property {Object} [parent] - Parent element ref for portal rendering
5652
+ * @property {String} [direction='right'] - Tooltip direction ( 'right', 'top')
5653
+ * @property {String} [position] - Custom CSS position override
5654
+ * @property {Number} [showDelay=250] - Delay in ms before showing tooltip on hover
5655
+ * @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
5656
+ * @property {*} [children] - Child elements to render inside the tooltip wrapper
5657
+ */
5658
+
5659
+ /**
5660
+ * Tooltip wrapper that provides context-based tooltip content lookup.
5661
+ * All props are forwarded to the underlying Tooltip component.
5662
+ *
5663
+ * @param {TooltipProps & {
5664
+ * forId: String,
5665
+ * value?: String|Object,
5666
+ * element?: Object
5667
+ * }} props - Shared tooltip props plus wrapper-specific ones
5668
+ */
5575
5669
  function TooltipWrapper(props) {
5576
5670
  const {
5577
5671
  forId,
@@ -5588,35 +5682,77 @@ function TooltipWrapper(props) {
5588
5682
  forId: `bio-properties-panel-${forId}`
5589
5683
  });
5590
5684
  }
5685
+
5686
+ /**
5687
+ * @param {TooltipProps & {
5688
+ * forId: String,
5689
+ * value: String|Object
5690
+ * }} props
5691
+ */
5591
5692
  function Tooltip(props) {
5592
5693
  const {
5593
5694
  forId,
5594
5695
  value,
5595
5696
  parent,
5596
5697
  direction = 'right',
5597
- position
5698
+ position,
5699
+ showDelay = 250,
5700
+ hideDelay = 250
5598
5701
  } = props;
5599
5702
  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;
5703
+ const showTimeoutRef = useRef(null);
5704
+ const hideTimeoutRef = useRef(null);
5604
5705
  const wrapperRef = useRef(null);
5605
5706
  const tooltipRef = useRef(null);
5606
5707
  const show = (_, delay) => {
5708
+ clearTimeout(showTimeoutRef.current);
5709
+ clearTimeout(hideTimeoutRef.current);
5607
5710
  if (visible) return;
5608
5711
  if (delay) {
5609
- timeout = setTimeout(() => {
5712
+ showTimeoutRef.current = setTimeout(() => {
5610
5713
  setVisible(true);
5611
- }, SHOW_DELAY);
5714
+ }, showDelay);
5612
5715
  } else {
5613
5716
  setVisible(true);
5614
5717
  }
5615
5718
  };
5616
- const hide = () => {
5617
- clearTimeout(timeout);
5618
- setVisible(false);
5719
+ const handleWrapperMouseEnter = e => {
5720
+ show(e, true);
5721
+ };
5722
+ const hide = (delay = false) => {
5723
+ clearTimeout(showTimeoutRef.current);
5724
+ clearTimeout(hideTimeoutRef.current);
5725
+ if (delay) {
5726
+ hideTimeoutRef.current = setTimeout(() => {
5727
+ setVisible(false);
5728
+ }, hideDelay);
5729
+ } else {
5730
+ setVisible(false);
5731
+ }
5619
5732
  };
5733
+
5734
+ // Cleanup timeouts on unmount
5735
+ useEffect(() => {
5736
+ return () => {
5737
+ clearTimeout(showTimeoutRef.current);
5738
+ clearTimeout(hideTimeoutRef.current);
5739
+ };
5740
+ }, []);
5741
+
5742
+ // Handle click outside to close tooltip for non-focusable elements
5743
+ useEffect(() => {
5744
+ if (!visible) return;
5745
+ const handleClickOutside = e => {
5746
+ // If clicking outside both the wrapper and tooltip, hide it
5747
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target) && tooltipRef.current && !tooltipRef.current.contains(e.target)) {
5748
+ hide(false);
5749
+ }
5750
+ };
5751
+ document.addEventListener('mousedown', handleClickOutside);
5752
+ return () => {
5753
+ document.removeEventListener('mousedown', handleClickOutside);
5754
+ };
5755
+ }, [visible, hide]);
5620
5756
  const handleMouseLeave = ({
5621
5757
  relatedTarget
5622
5758
  }) => {
@@ -5624,23 +5760,32 @@ function Tooltip(props) {
5624
5760
  if (relatedTarget === wrapperRef.current || relatedTarget === tooltipRef.current || relatedTarget?.parentElement === tooltipRef.current) {
5625
5761
  return;
5626
5762
  }
5627
- hide();
5763
+ const selection = window.getSelection();
5764
+ if (selection && selection.toString().length > 0) {
5765
+ // Check if selection is within tooltip content
5766
+ const selectionRange = selection.getRangeAt(0);
5767
+ if (tooltipRef.current?.contains(selectionRange.commonAncestorContainer) || tooltipRef.current?.contains(selection.anchorNode) || tooltipRef.current?.contains(selection.focusNode)) {
5768
+ return; // Keep tooltip open during text selection
5769
+ }
5770
+ }
5771
+ hide(true);
5772
+ };
5773
+ const handleTooltipMouseEnter = () => {
5774
+ clearTimeout(hideTimeoutRef.current);
5628
5775
  };
5629
5776
  const handleFocusOut = e => {
5630
5777
  const {
5631
- target
5778
+ relatedTarget
5632
5779
  } = e;
5633
5780
 
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();
5781
+ // Don't hide if focus moved to the tooltip or another element within the wrapper
5782
+ if (tooltipRef.current?.contains(relatedTarget) || wrapperRef.current?.contains(relatedTarget)) {
5638
5783
  return;
5639
5784
  }
5640
- hide();
5785
+ hide(false);
5641
5786
  };
5642
5787
  const hideTooltipViaEscape = e => {
5643
- e.code === 'Escape' && hide();
5788
+ e.code === 'Escape' && hide(false);
5644
5789
  };
5645
5790
  const renderTooltip = () => {
5646
5791
  return jsxs("div", {
@@ -5651,6 +5796,7 @@ function Tooltip(props) {
5651
5796
  style: position || getTooltipPosition(wrapperRef.current),
5652
5797
  ref: tooltipRef,
5653
5798
  onClick: e => e.stopPropagation(),
5799
+ onMouseEnter: handleTooltipMouseEnter,
5654
5800
  onMouseLeave: handleMouseLeave,
5655
5801
  children: [jsx("div", {
5656
5802
  class: "bio-properties-panel-tooltip-content",
@@ -5664,7 +5810,7 @@ function Tooltip(props) {
5664
5810
  class: "bio-properties-panel-tooltip-wrapper",
5665
5811
  tabIndex: "0",
5666
5812
  ref: wrapperRef,
5667
- onMouseEnter: e => show(e, true),
5813
+ onMouseEnter: handleWrapperMouseEnter,
5668
5814
  onMouseLeave: handleMouseLeave,
5669
5815
  onFocus: show,
5670
5816
  onBlur: handleFocusOut,
@@ -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,
@@ -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,
@@ -7356,6 +7543,12 @@ function FeelTextfield(props) {
7356
7543
  const [localValue, setLocalValue] = useState(value);
7357
7544
  const editorRef = useShowEntryEvent(id);
7358
7545
  const containerRef = useRef();
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]);
7359
7552
  const feelActive = isString(localValue) && localValue.startsWith('=') || feel === 'required';
7360
7553
  const feelOnlyValue = isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
7361
7554
  const feelLanguageContext = useContext(FeelLanguageContext);
@@ -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);
@@ -7531,6 +7742,7 @@ function FeelTextfield(props) {
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,25 @@ function isEdited$5(node) {
7979
8194
  function prefixId$5(id) {
7980
8195
  return `bio-properties-panel-${id}`;
7981
8196
  }
8197
+
8198
+ /**
8199
+ * @typedef { { value: string, label: string, disabled: boolean, children: { value: string, label: string, disabled: boolean } } } Option
8200
+ */
8201
+
8202
+ /**
8203
+ * Provides basic select input.
8204
+ *
8205
+ * @param {object} props
8206
+ * @param {string} props.id
8207
+ * @param {string[]} props.path
8208
+ * @param {string} props.label
8209
+ * @param {Function} props.onChange
8210
+ * @param {Function} props.onFocus
8211
+ * @param {Function} props.onBlur
8212
+ * @param {Array<Option>} [props.options]
8213
+ * @param {string} props.value
8214
+ * @param {boolean} [props.disabled]
8215
+ */
7982
8216
  function Select(props) {
7983
8217
  const {
7984
8218
  id,
@@ -8144,12 +8378,13 @@ function TextArea(props) {
8144
8378
  id,
8145
8379
  label,
8146
8380
  debounce,
8147
- onInput,
8381
+ onInput: commitValue,
8148
8382
  value = '',
8149
8383
  disabled,
8150
8384
  monospace,
8151
8385
  onFocus,
8152
8386
  onBlur,
8387
+ onPaste,
8153
8388
  autoResize = true,
8154
8389
  placeholder,
8155
8390
  rows = autoResize ? 1 : 2,
@@ -8157,16 +8392,16 @@ function TextArea(props) {
8157
8392
  } = props;
8158
8393
  const [localValue, setLocalValue] = useState(value);
8159
8394
  const ref = useShowEntryEvent(id);
8395
+ const onInput = useCallback(newValue => {
8396
+ const newModelValue = newValue === '' ? undefined : newValue;
8397
+ commitValue(newModelValue);
8398
+ }, [commitValue]);
8160
8399
  const visible = useElementVisible(ref.current);
8161
8400
 
8162
8401
  /**
8163
8402
  * @type { import('min-dash').DebouncedFunction }
8164
8403
  */
8165
- const handleInputCallback = useDebounce(onInput, debounce);
8166
- const handleInput = newValue => {
8167
- const newModelValue = newValue === '' ? undefined : newValue;
8168
- handleInputCallback(newModelValue);
8169
- };
8404
+ const handleInput = useDebounce(onInput, debounce);
8170
8405
  const handleLocalInput = e => {
8171
8406
  autoResize && resizeToContents(e.target);
8172
8407
  if (e.target.value === localValue) {
@@ -8179,11 +8414,40 @@ function TextArea(props) {
8179
8414
  const trimmedValue = e.target.value.trim();
8180
8415
 
8181
8416
  // trim and commit on blur
8417
+ handleInput.cancel?.();
8182
8418
  onInput(trimmedValue);
8419
+ setLocalValue(trimmedValue);
8183
8420
  if (onBlur) {
8184
8421
  onBlur(e);
8185
8422
  }
8186
8423
  };
8424
+ const handleOnPaste = e => {
8425
+ const input = e.target;
8426
+ const isFieldEmpty = !input.value;
8427
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
8428
+
8429
+ // Trim and handle paste if field is empty or all content is selected
8430
+ if (isFieldEmpty || isAllSelected) {
8431
+ const trimmedValue = e.clipboardData.getData('text').trim();
8432
+ setLocalValue(trimmedValue);
8433
+ handleInput(trimmedValue);
8434
+ if (onPaste) {
8435
+ onPaste(e);
8436
+ }
8437
+ e.preventDefault();
8438
+ return;
8439
+ }
8440
+
8441
+ // Allow default paste behavior for normal text editing
8442
+ if (onPaste) {
8443
+ onPaste(e);
8444
+ }
8445
+ };
8446
+ const handleOnKeyDown = e => {
8447
+ if (isCmdWithChar(e)) {
8448
+ handleInput.flush();
8449
+ }
8450
+ };
8187
8451
  useLayoutEffect(() => {
8188
8452
  autoResize && resizeToContents(ref.current);
8189
8453
  }, []);
@@ -8215,7 +8479,9 @@ function TextArea(props) {
8215
8479
  class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
8216
8480
  onInput: handleLocalInput,
8217
8481
  onFocus: onFocus,
8482
+ onKeyDown: handleOnKeyDown,
8218
8483
  onBlur: handleOnBlur,
8484
+ onPaste: handleOnPaste,
8219
8485
  placeholder: placeholder,
8220
8486
  rows: rows,
8221
8487
  value: localValue,
@@ -8236,6 +8502,7 @@ function TextArea(props) {
8236
8502
  * @param {Function} props.setValue
8237
8503
  * @param {Function} props.onFocus
8238
8504
  * @param {Function} props.onBlur
8505
+ * @param {Function} props.onPaste
8239
8506
  * @param {number} props.rows
8240
8507
  * @param {boolean} props.monospace
8241
8508
  * @param {Function} [props.validate]
@@ -8256,6 +8523,7 @@ function TextAreaEntry(props) {
8256
8523
  validate,
8257
8524
  onFocus,
8258
8525
  onBlur,
8526
+ onPaste,
8259
8527
  placeholder,
8260
8528
  autoResize,
8261
8529
  tooltip
@@ -8291,6 +8559,7 @@ function TextAreaEntry(props) {
8291
8559
  onInput: onInput,
8292
8560
  onFocus: onFocus,
8293
8561
  onBlur: onBlur,
8562
+ onPaste: onPaste,
8294
8563
  rows: rows,
8295
8564
  debounce: debounce,
8296
8565
  monospace: monospace,
@@ -8324,32 +8593,57 @@ function Textfield(props) {
8324
8593
  disabled = false,
8325
8594
  id,
8326
8595
  label,
8327
- onInput,
8596
+ onInput: commitValue,
8328
8597
  onFocus,
8329
8598
  onBlur,
8599
+ onPaste,
8330
8600
  placeholder,
8331
8601
  value = '',
8332
8602
  tooltip
8333
8603
  } = props;
8334
8604
  const [localValue, setLocalValue] = useState(value || '');
8335
8605
  const ref = useShowEntryEvent(id);
8606
+ const onInput = useCallback(newValue => {
8607
+ const newModelValue = newValue === '' ? undefined : newValue;
8608
+ commitValue(newModelValue);
8609
+ }, [commitValue]);
8336
8610
 
8337
8611
  /**
8338
8612
  * @type { import('min-dash').DebouncedFunction }
8339
8613
  */
8340
- const handleInputCallback = useDebounce(onInput, debounce);
8614
+ const handleInput = useDebounce(onInput, debounce);
8341
8615
  const handleOnBlur = e => {
8342
8616
  const trimmedValue = e.target.value.trim();
8343
8617
 
8344
8618
  // trim and commit on blur
8619
+ handleInput.cancel?.();
8345
8620
  onInput(trimmedValue);
8621
+ setLocalValue(trimmedValue);
8346
8622
  if (onBlur) {
8347
8623
  onBlur(e);
8348
8624
  }
8349
8625
  };
8350
- const handleInput = newValue => {
8351
- const newModelValue = newValue === '' ? undefined : newValue;
8352
- handleInputCallback(newModelValue);
8626
+ const handleOnPaste = e => {
8627
+ const input = e.target;
8628
+ const isFieldEmpty = !input.value;
8629
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
8630
+
8631
+ // Trim and handle paste if field is empty or all content is selected (overwrite)
8632
+ if (isFieldEmpty || isAllSelected) {
8633
+ const trimmedValue = e.clipboardData.getData('text').trim();
8634
+ setLocalValue(trimmedValue);
8635
+ handleInput(trimmedValue);
8636
+ if (onPaste) {
8637
+ onPaste(e);
8638
+ }
8639
+ e.preventDefault();
8640
+ return;
8641
+ }
8642
+
8643
+ // Allow default paste behavior for normal text editing
8644
+ if (onPaste) {
8645
+ onPaste(e);
8646
+ }
8353
8647
  };
8354
8648
  const handleLocalInput = e => {
8355
8649
  if (e.target.value === localValue) {
@@ -8364,6 +8658,11 @@ function Textfield(props) {
8364
8658
  }
8365
8659
  setLocalValue(value);
8366
8660
  }, [value]);
8661
+ const handleOnKeyDown = e => {
8662
+ if (isCmdWithChar(e)) {
8663
+ handleInput.flush();
8664
+ }
8665
+ };
8367
8666
  return jsxs("div", {
8368
8667
  class: "bio-properties-panel-textfield",
8369
8668
  children: [jsx("label", {
@@ -8386,7 +8685,9 @@ function Textfield(props) {
8386
8685
  class: "bio-properties-panel-input",
8387
8686
  onInput: handleLocalInput,
8388
8687
  onFocus: onFocus,
8688
+ onKeyDown: handleOnKeyDown,
8389
8689
  onBlur: handleOnBlur,
8690
+ onPaste: handleOnPaste,
8390
8691
  placeholder: placeholder,
8391
8692
  value: localValue
8392
8693
  })]
@@ -8421,6 +8722,7 @@ function TextfieldEntry(props) {
8421
8722
  validate,
8422
8723
  onFocus,
8423
8724
  onBlur,
8725
+ onPaste,
8424
8726
  placeholder,
8425
8727
  tooltip
8426
8728
  } = props;
@@ -8456,6 +8758,7 @@ function TextfieldEntry(props) {
8456
8758
  onInput: onInput,
8457
8759
  onFocus: onFocus,
8458
8760
  onBlur: onBlur,
8761
+ onPaste: onPaste,
8459
8762
  placeholder: placeholder,
8460
8763
  value: value,
8461
8764
  tooltip: tooltip,
@@ -8766,6 +9069,25 @@ function cancel(event) {
8766
9069
  event.preventDefault();
8767
9070
  event.stopPropagation();
8768
9071
  }
9072
+
9073
+ /**
9074
+ * @typedef {Object} FeelPopupProps
9075
+ * @property {string} entryId
9076
+ * @property {Function} onInput
9077
+ * @property {Function} onClose
9078
+ * @property {string} title
9079
+ * @property {'feel'|'feelers'} type
9080
+ * @property {string} value
9081
+ * @property {Array} [links]
9082
+ * @property {Array|Object} [variables]
9083
+ * @property {Object} [position]
9084
+ * @property {string} [hostLanguage]
9085
+ * @property {boolean} [singleLine]
9086
+ * @property {HTMLElement} [sourceElement]
9087
+ * @property {HTMLElement|string} [tooltipContainer]
9088
+ * @property {Object} [eventBus]
9089
+ */
9090
+
8769
9091
  const FEEL_POPUP_WIDTH = 700;
8770
9092
  const FEEL_POPUP_HEIGHT = 250;
8771
9093