@bpmn-io/properties-panel 3.33.2 → 3.35.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.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useContext, useState, useRef, useCallback, useEffect, useMemo, useLayoutEffect } from '../preact/hooks';
1
+ import { useContext, useState, useRef, useEffect, useCallback, useMemo, useLayoutEffect } from '../preact/hooks';
2
2
  import { isFunction, isArray, get, assign, set, isString, isNumber, debounce } from 'min-dash';
3
3
  import { createPortal, forwardRef } from '../preact/compat';
4
4
  import { jsx, jsxs, Fragment } from '../preact/jsx-runtime';
@@ -153,6 +153,20 @@ OpenPopupIcon.defaultProps = {
153
153
  viewBox: "0 0 16 16"
154
154
  };
155
155
 
156
+ /**
157
+ * @typedef { {
158
+ * getElementLabel: (element: object) => string,
159
+ * getTypeLabel: (element: object) => string,
160
+ * getElementIcon: (element: object) => import('preact').Component,
161
+ * getDocumentationRef: (element: object) => string
162
+ * } } HeaderProvider
163
+ */
164
+
165
+ /**
166
+ * @param {Object} props
167
+ * @param {Object} props.element,
168
+ * @param {HeaderProvider} props.headerProvider
169
+ */
156
170
  function Header(props) {
157
171
  const {
158
172
  element,
@@ -180,11 +194,9 @@ function Header(props) {
180
194
  }), jsxs("div", {
181
195
  class: "bio-properties-panel-header-labels",
182
196
  children: [jsx("div", {
183
- title: type,
184
197
  class: "bio-properties-panel-header-type",
185
198
  children: type
186
199
  }), label ? jsx("div", {
187
- title: label,
188
200
  class: "bio-properties-panel-header-label",
189
201
  children: label
190
202
  }) : null]
@@ -277,6 +289,26 @@ function useTooltipContext(id, element) {
277
289
  return getTooltipForId(id, element);
278
290
  }
279
291
 
292
+ /**
293
+ * @typedef {Object} TooltipProps
294
+ * @property {Object} [parent] - Parent element ref for portal rendering
295
+ * @property {String} [direction='right'] - Tooltip direction ( 'right', 'top')
296
+ * @property {String} [position] - Custom CSS position override
297
+ * @property {Number} [showDelay=250] - Delay in ms before showing tooltip on hover
298
+ * @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
299
+ * @property {*} [children] - Child elements to render inside the tooltip wrapper
300
+ */
301
+
302
+ /**
303
+ * Tooltip wrapper that provides context-based tooltip content lookup.
304
+ * All props are forwarded to the underlying Tooltip component.
305
+ *
306
+ * @param {TooltipProps & {
307
+ * forId: String,
308
+ * value?: String|Object,
309
+ * element?: Object
310
+ * }} props - Shared tooltip props plus wrapper-specific ones
311
+ */
280
312
  function TooltipWrapper(props) {
281
313
  const {
282
314
  forId,
@@ -293,35 +325,77 @@ function TooltipWrapper(props) {
293
325
  forId: `bio-properties-panel-${forId}`
294
326
  });
295
327
  }
328
+
329
+ /**
330
+ * @param {TooltipProps & {
331
+ * forId: String,
332
+ * value: String|Object
333
+ * }} props
334
+ */
296
335
  function Tooltip(props) {
297
336
  const {
298
337
  forId,
299
338
  value,
300
339
  parent,
301
340
  direction = 'right',
302
- position
341
+ position,
342
+ showDelay = 250,
343
+ hideDelay = 250
303
344
  } = props;
304
345
  const [visible, setVisible] = useState(false);
305
-
306
- // Tooltip will be shown after SHOW_DELAY ms from hovering over the source element.
307
- const SHOW_DELAY = 200;
308
- let timeout = null;
346
+ const showTimeoutRef = useRef(null);
347
+ const hideTimeoutRef = useRef(null);
309
348
  const wrapperRef = useRef(null);
310
349
  const tooltipRef = useRef(null);
311
350
  const show = (_, delay) => {
351
+ clearTimeout(showTimeoutRef.current);
352
+ clearTimeout(hideTimeoutRef.current);
312
353
  if (visible) return;
313
354
  if (delay) {
314
- timeout = setTimeout(() => {
355
+ showTimeoutRef.current = setTimeout(() => {
315
356
  setVisible(true);
316
- }, SHOW_DELAY);
357
+ }, showDelay);
317
358
  } else {
318
359
  setVisible(true);
319
360
  }
320
361
  };
321
- const hide = () => {
322
- clearTimeout(timeout);
323
- setVisible(false);
362
+ const handleWrapperMouseEnter = e => {
363
+ show(e, true);
364
+ };
365
+ const hide = (delay = false) => {
366
+ clearTimeout(showTimeoutRef.current);
367
+ clearTimeout(hideTimeoutRef.current);
368
+ if (delay) {
369
+ hideTimeoutRef.current = setTimeout(() => {
370
+ setVisible(false);
371
+ }, hideDelay);
372
+ } else {
373
+ setVisible(false);
374
+ }
324
375
  };
376
+
377
+ // Cleanup timeouts on unmount
378
+ useEffect(() => {
379
+ return () => {
380
+ clearTimeout(showTimeoutRef.current);
381
+ clearTimeout(hideTimeoutRef.current);
382
+ };
383
+ }, []);
384
+
385
+ // Handle click outside to close tooltip for non-focusable elements
386
+ useEffect(() => {
387
+ if (!visible) return;
388
+ const handleClickOutside = e => {
389
+ // If clicking outside both the wrapper and tooltip, hide it
390
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target) && tooltipRef.current && !tooltipRef.current.contains(e.target)) {
391
+ hide(false);
392
+ }
393
+ };
394
+ document.addEventListener('mousedown', handleClickOutside);
395
+ return () => {
396
+ document.removeEventListener('mousedown', handleClickOutside);
397
+ };
398
+ }, [visible, hide]);
325
399
  const handleMouseLeave = ({
326
400
  relatedTarget
327
401
  }) => {
@@ -329,23 +403,32 @@ function Tooltip(props) {
329
403
  if (relatedTarget === wrapperRef.current || relatedTarget === tooltipRef.current || relatedTarget?.parentElement === tooltipRef.current) {
330
404
  return;
331
405
  }
332
- hide();
406
+ const selection = window.getSelection();
407
+ if (selection && selection.toString().length > 0) {
408
+ // Check if selection is within tooltip content
409
+ const selectionRange = selection.getRangeAt(0);
410
+ if (tooltipRef.current?.contains(selectionRange.commonAncestorContainer) || tooltipRef.current?.contains(selection.anchorNode) || tooltipRef.current?.contains(selection.focusNode)) {
411
+ return; // Keep tooltip open during text selection
412
+ }
413
+ }
414
+ hide(true);
415
+ };
416
+ const handleTooltipMouseEnter = () => {
417
+ clearTimeout(hideTimeoutRef.current);
333
418
  };
334
419
  const handleFocusOut = e => {
335
420
  const {
336
- target
421
+ relatedTarget
337
422
  } = e;
338
423
 
339
- // Don't hide the tooltip if the wrapper or the tooltip itself is clicked.
340
- const isHovered = target.matches(':hover') || tooltipRef.current?.matches(':hover');
341
- if (target === wrapperRef.current && isHovered) {
342
- e.stopPropagation();
424
+ // Don't hide if focus moved to the tooltip or another element within the wrapper
425
+ if (tooltipRef.current?.contains(relatedTarget) || wrapperRef.current?.contains(relatedTarget)) {
343
426
  return;
344
427
  }
345
- hide();
428
+ hide(false);
346
429
  };
347
430
  const hideTooltipViaEscape = e => {
348
- e.code === 'Escape' && hide();
431
+ e.code === 'Escape' && hide(false);
349
432
  };
350
433
  const renderTooltip = () => {
351
434
  return jsxs("div", {
@@ -356,6 +439,7 @@ function Tooltip(props) {
356
439
  style: position || getTooltipPosition(wrapperRef.current),
357
440
  ref: tooltipRef,
358
441
  onClick: e => e.stopPropagation(),
442
+ onMouseEnter: handleTooltipMouseEnter,
359
443
  onMouseLeave: handleMouseLeave,
360
444
  children: [jsx("div", {
361
445
  class: "bio-properties-panel-tooltip-content",
@@ -369,7 +453,7 @@ function Tooltip(props) {
369
453
  class: "bio-properties-panel-tooltip-wrapper",
370
454
  tabIndex: "0",
371
455
  ref: wrapperRef,
372
- onMouseEnter: e => show(e, true),
456
+ onMouseEnter: handleWrapperMouseEnter,
373
457
  onMouseLeave: handleMouseLeave,
374
458
  onFocus: show,
375
459
  onBlur: handleFocusOut,
@@ -468,8 +552,6 @@ function useEvent(event, callback, eventBus) {
468
552
  }, [callback, event, eventBus]);
469
553
  }
470
554
 
471
- const KEY_LENGTH = 6;
472
-
473
555
  /**
474
556
  * Create a persistent key factory for plain objects without id.
475
557
  *
@@ -494,7 +576,7 @@ function useKeyFactory(dependencies = []) {
494
576
  const getKey = el => {
495
577
  let key = map.get(el);
496
578
  if (!key) {
497
- key = Math.random().toString().slice(-KEY_LENGTH);
579
+ key = Math.random().toString().slice(-6);
498
580
  map.set(el, key);
499
581
  }
500
582
  return key;
@@ -685,6 +767,9 @@ function useElementVisible(element) {
685
767
  return visible;
686
768
  }
687
769
 
770
+ /**
771
+ * @param {import('../PropertiesPanel').GroupDefinition} props
772
+ */
688
773
  function Group(props) {
689
774
  const {
690
775
  element,
@@ -739,8 +824,6 @@ function Group(props) {
739
824
  class: classnames('bio-properties-panel-group-header', edited ? '' : 'empty', open ? 'open' : '', sticky && open ? 'sticky' : ''),
740
825
  onClick: toggleOpen,
741
826
  children: [jsx("div", {
742
- title: props.tooltip ? null : label,
743
- "data-title": label,
744
827
  class: "bio-properties-panel-group-header-title",
745
828
  children: jsx(TooltipWrapper, {
746
829
  value: props.tooltip,
@@ -1093,6 +1176,13 @@ function useUpdateLayoutEffect(effect, deps) {
1093
1176
  }, deps);
1094
1177
  }
1095
1178
 
1179
+ /**
1180
+ *
1181
+ * @param {object} props
1182
+ * @param {string} [props.class]
1183
+ * @param {import('preact').Component[]} [props.menuItems]
1184
+ * @returns
1185
+ */
1096
1186
  function DropdownButton(props) {
1097
1187
  const {
1098
1188
  class: className,
@@ -1250,7 +1340,6 @@ function CollapsibleEntry(props) {
1250
1340
  class: "bio-properties-panel-collapsible-entry-header",
1251
1341
  onClick: toggleOpen,
1252
1342
  children: [jsx("div", {
1253
- title: label || placeholderLabel,
1254
1343
  class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
1255
1344
  children: label || placeholderLabel
1256
1345
  }), jsx("button", {
@@ -1287,6 +1376,9 @@ function CollapsibleEntry(props) {
1287
1376
  });
1288
1377
  }
1289
1378
 
1379
+ /**
1380
+ * @param {import('../PropertiesPanel').ListItemDefinition} props
1381
+ */
1290
1382
  function ListItem(props) {
1291
1383
  const {
1292
1384
  autoFocusEntry,
@@ -1389,8 +1481,6 @@ function ListGroup(props) {
1389
1481
  class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
1390
1482
  onClick: hasItems ? toggleOpen : noop$6,
1391
1483
  children: [jsx("div", {
1392
- title: props.tooltip ? null : label,
1393
- "data-title": label,
1394
1484
  class: "bio-properties-panel-group-header-title",
1395
1485
  children: jsx(TooltipWrapper, {
1396
1486
  value: props.tooltip,
@@ -1460,6 +1550,12 @@ function getNewItemIds(newItems, oldItems) {
1460
1550
  return newIds.filter(itemId => !oldIds.includes(itemId));
1461
1551
  }
1462
1552
 
1553
+ /**
1554
+ * @param {Object} props
1555
+ * @param {Object} props.element
1556
+ * @param {String} props.forId - id of the entry the description is used for
1557
+ * @param {String} props.value
1558
+ */
1463
1559
  function Description(props) {
1464
1560
  const {
1465
1561
  element,
@@ -1591,6 +1687,15 @@ function prefixId$8(id) {
1591
1687
  return `bio-properties-panel-${id}`;
1592
1688
  }
1593
1689
 
1690
+ /**
1691
+ * Button to open popups.
1692
+ *
1693
+ * @param {Object} props
1694
+ * @param {Function} props.onClick - Callback to trigger when the button is clicked.
1695
+ * @param {string} [props.title] - Tooltip text for the button.
1696
+ * @param {boolean} [props.disabled] - Whether the button is disabled.
1697
+ * @param {string} [props.className] - Additional class names for the button.
1698
+ */
1594
1699
  function OpenPopupButton({
1595
1700
  onClick,
1596
1701
  title = 'Open pop-up editor'
@@ -1736,6 +1841,7 @@ const FeelEditor = forwardRef((props, ref) => {
1736
1841
  enableGutters,
1737
1842
  value,
1738
1843
  onInput,
1844
+ onKeyDown: onKeyDownProp = noop$4,
1739
1845
  onFeelToggle = noop$4,
1740
1846
  onLint = noop$4,
1741
1847
  onOpenPopup = noop$4,
@@ -1768,6 +1874,8 @@ const FeelEditor = forwardRef((props, ref) => {
1768
1874
  * - AND the cursor is at the beginning of the input
1769
1875
  */
1770
1876
  const onKeyDown = e => {
1877
+ // Call parent onKeyDown handler first
1878
+ onKeyDownProp(e);
1771
1879
  if (e.key !== 'Backspace' || !editor) {
1772
1880
  return;
1773
1881
  }
@@ -1887,6 +1995,22 @@ function FeelIcon(props) {
1887
1995
  });
1888
1996
  }
1889
1997
 
1998
+ /**
1999
+ * @param {KeyboardEvent} event
2000
+ * @return {boolean}
2001
+ */
2002
+ function isCmd(event) {
2003
+ // ensure we don't react to AltGr
2004
+ // (mapped to CTRL + ALT)
2005
+ if (event.altKey) {
2006
+ return false;
2007
+ }
2008
+ return event.ctrlKey || event.metaKey;
2009
+ }
2010
+ function isCmdWithChar(event) {
2011
+ return isCmd(event) && event.key.length === 1 && /^[a-zA-Z]$/.test(event.key);
2012
+ }
2013
+
1890
2014
  function ToggleSwitch(props) {
1891
2015
  const {
1892
2016
  id,
@@ -2216,6 +2340,7 @@ function FeelTextfield(props) {
2216
2340
  const [localValue, setLocalValue] = useState(value);
2217
2341
  const editorRef = useShowEntryEvent(id);
2218
2342
  const containerRef = useRef();
2343
+ const isTogglingFromPasteRef = useRef(false);
2219
2344
  const onInput = useCallback(newValue => {
2220
2345
  // we don't commit empty FEEL expressions,
2221
2346
  // but instead serialize them as <undefined>
@@ -2254,31 +2379,43 @@ function FeelTextfield(props) {
2254
2379
  handleInput(feelOnlyValue);
2255
2380
  }
2256
2381
  });
2257
- const handleLocalInput = newValue => {
2382
+ const handleLocalInput = (newValue, useDebounce = true) => {
2258
2383
  if (feelActive) {
2259
2384
  newValue = '=' + newValue;
2260
2385
  }
2261
- if (newValue === localValue) {
2262
- return;
2263
- }
2264
2386
  setLocalValue(newValue);
2265
- handleInput(newValue);
2387
+ if (useDebounce) {
2388
+ handleInput(newValue);
2389
+ } else {
2390
+ onInput(newValue);
2391
+ }
2266
2392
  if (!feelActive && isString(newValue) && newValue.startsWith('=')) {
2267
2393
  // focus is behind `=` sign that will be removed
2268
2394
  setFocus(-1);
2269
2395
  }
2270
2396
  };
2271
2397
  const handleOnBlur = e => {
2398
+ // Ignore blur when toggling from paste to avoid interference
2399
+ if (isTogglingFromPasteRef.current) {
2400
+ isTogglingFromPasteRef.current = false;
2401
+ return;
2402
+ }
2403
+ handleInput.cancel?.();
2272
2404
  if (e.target.type === 'checkbox') {
2273
2405
  onInput(e.target.checked);
2274
2406
  } else {
2275
2407
  const trimmedValue = e.target.value.trim();
2276
- onInput(trimmedValue);
2408
+ handleLocalInput(trimmedValue, false);
2277
2409
  }
2278
2410
  if (onBlur) {
2279
2411
  onBlur(e);
2280
2412
  }
2281
2413
  };
2414
+ const handleOnKeyDown = e => {
2415
+ if (isCmdWithChar(e)) {
2416
+ handleInput.flush();
2417
+ }
2418
+ };
2282
2419
  const handleLint = useStaticCallback((lint = []) => {
2283
2420
  const syntaxError = lint.some(report => report.type === 'Syntax Error');
2284
2421
  if (syntaxError) {
@@ -2345,12 +2482,27 @@ function FeelTextfield(props) {
2345
2482
  if (feelActive || isPopupOpen) {
2346
2483
  return;
2347
2484
  }
2348
- const data = event.clipboardData.getData('application/FEEL');
2349
- if (data) {
2485
+ const feelData = event.clipboardData.getData('application/FEEL');
2486
+ if (feelData) {
2350
2487
  setTimeout(() => {
2351
2488
  handleFeelToggle();
2352
2489
  setFocus();
2353
2490
  });
2491
+ return;
2492
+ }
2493
+ const input = event.target;
2494
+ const isFieldEmpty = !input.value;
2495
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
2496
+ if (isFieldEmpty || isAllSelected) {
2497
+ const textData = event.clipboardData.getData('text');
2498
+ const trimmedValue = textData.trim();
2499
+ setLocalValue(trimmedValue);
2500
+ handleInput(trimmedValue);
2501
+ if (!feelActive && isString(trimmedValue) && trimmedValue.startsWith('=')) {
2502
+ isTogglingFromPasteRef.current = true;
2503
+ setFocus(trimmedValue.length - 1);
2504
+ }
2505
+ event.preventDefault();
2354
2506
  }
2355
2507
  };
2356
2508
  containerRef.current.addEventListener('copy', copyHandler);
@@ -2391,6 +2543,7 @@ function FeelTextfield(props) {
2391
2543
  }), feelActive ? jsx(FeelEditor, {
2392
2544
  name: id,
2393
2545
  onInput: handleLocalInput,
2546
+ onKeyDown: handleOnKeyDown,
2394
2547
  contentAttributes: {
2395
2548
  'id': prefixId$5(id),
2396
2549
  'aria-label': label
@@ -2413,6 +2566,7 @@ function FeelTextfield(props) {
2413
2566
  ...props,
2414
2567
  popupOpen: isPopupOpen,
2415
2568
  onInput: handleLocalInput,
2569
+ onKeyDown: handleOnKeyDown,
2416
2570
  onBlur: handleOnBlur,
2417
2571
  contentAttributes: {
2418
2572
  'id': prefixId$5(id),
@@ -2431,6 +2585,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
2431
2585
  id,
2432
2586
  disabled,
2433
2587
  onInput,
2588
+ onKeyDown,
2434
2589
  value,
2435
2590
  onFocus,
2436
2591
  onBlur,
@@ -2466,6 +2621,7 @@ const OptionalFeelInput = forwardRef((props, ref) => {
2466
2621
  class: "bio-properties-panel-input",
2467
2622
  onInput: e => onInput(e.target.value),
2468
2623
  onFocus: onFocus,
2624
+ onKeyDown: onKeyDown,
2469
2625
  onBlur: onBlur,
2470
2626
  placeholder: placeholder,
2471
2627
  value: value || ''
@@ -3060,6 +3216,22 @@ function prefixIdLabel(id) {
3060
3216
  return `bio-properties-panel-feelers-${id}-label`;
3061
3217
  }
3062
3218
 
3219
+ /**
3220
+ * Entry for handling lists represented as nested entries.
3221
+ *
3222
+ * @template Item
3223
+ * @param {object} props
3224
+ * @param {string} props.id
3225
+ * @param {*} props.element
3226
+ * @param {Function} props.onAdd
3227
+ * @param {import('preact').Component} props.component
3228
+ * @param {string} [props.label='<empty>']
3229
+ * @param {Function} [props.onRemove]
3230
+ * @param {Item[]} [props.items]
3231
+ * @param {boolean} [props.open]
3232
+ * @param {string|boolean} [props.autoFocusEntry] either a custom selector string or true to focus the first input
3233
+ * @returns
3234
+ */
3063
3235
  function List(props) {
3064
3236
  const {
3065
3237
  id,
@@ -3107,7 +3279,6 @@ function List(props) {
3107
3279
  class: classnames('bio-properties-panel-list-entry-header', sticky && open ? 'sticky' : ''),
3108
3280
  onClick: toggleOpen,
3109
3281
  children: [jsx("div", {
3110
- title: label,
3111
3282
  class: classnames('bio-properties-panel-list-entry-header-title', open && 'open'),
3112
3283
  children: label
3113
3284
  }), jsxs("div", {
@@ -3212,6 +3383,24 @@ function useNewItems(items = [], shouldReset) {
3212
3383
  return previousItems ? items.filter(item => !previousItems.includes(item)) : [];
3213
3384
  }
3214
3385
 
3386
+ /**
3387
+ * @typedef { { value: string, label: string, disabled: boolean, children: { value: string, label: string, disabled: boolean } } } Option
3388
+ */
3389
+
3390
+ /**
3391
+ * Provides basic select input.
3392
+ *
3393
+ * @param {object} props
3394
+ * @param {string} props.id
3395
+ * @param {string[]} props.path
3396
+ * @param {string} props.label
3397
+ * @param {Function} props.onChange
3398
+ * @param {Function} props.onFocus
3399
+ * @param {Function} props.onBlur
3400
+ * @param {Array<Option>} [props.options]
3401
+ * @param {string} props.value
3402
+ * @param {boolean} [props.disabled]
3403
+ */
3215
3404
  function Select(props) {
3216
3405
  const {
3217
3406
  id,
@@ -3366,6 +3555,17 @@ function prefixId$4(id) {
3366
3555
  return `bio-properties-panel-${id}`;
3367
3556
  }
3368
3557
 
3558
+ /**
3559
+ * @param {Object} props
3560
+ * @param {Function} props.debounce
3561
+ * @param {Boolean} [props.disabled]
3562
+ * @param {Object} props.element
3563
+ * @param {Function} props.getValue
3564
+ * @param {String} props.id
3565
+ * @param {Function} [props.onBlur]
3566
+ * @param {Function} [props.onFocus]
3567
+ * @param {Function} props.setValue
3568
+ */
3369
3569
  function Simple(props) {
3370
3570
  const {
3371
3571
  debounce,
@@ -3438,6 +3638,7 @@ function TextArea(props) {
3438
3638
  monospace,
3439
3639
  onFocus,
3440
3640
  onBlur,
3641
+ onPaste,
3441
3642
  autoResize = true,
3442
3643
  placeholder,
3443
3644
  rows = autoResize ? 1 : 2,
@@ -3467,11 +3668,40 @@ function TextArea(props) {
3467
3668
  const trimmedValue = e.target.value.trim();
3468
3669
 
3469
3670
  // trim and commit on blur
3671
+ handleInput.cancel?.();
3470
3672
  onInput(trimmedValue);
3673
+ setLocalValue(trimmedValue);
3471
3674
  if (onBlur) {
3472
3675
  onBlur(e);
3473
3676
  }
3474
3677
  };
3678
+ const handleOnPaste = e => {
3679
+ const input = e.target;
3680
+ const isFieldEmpty = !input.value;
3681
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
3682
+
3683
+ // Trim and handle paste if field is empty or all content is selected
3684
+ if (isFieldEmpty || isAllSelected) {
3685
+ const trimmedValue = e.clipboardData.getData('text').trim();
3686
+ setLocalValue(trimmedValue);
3687
+ handleInput(trimmedValue);
3688
+ if (onPaste) {
3689
+ onPaste(e);
3690
+ }
3691
+ e.preventDefault();
3692
+ return;
3693
+ }
3694
+
3695
+ // Allow default paste behavior for normal text editing
3696
+ if (onPaste) {
3697
+ onPaste(e);
3698
+ }
3699
+ };
3700
+ const handleOnKeyDown = e => {
3701
+ if (isCmdWithChar(e)) {
3702
+ handleInput.flush();
3703
+ }
3704
+ };
3475
3705
  useLayoutEffect(() => {
3476
3706
  autoResize && resizeToContents(ref.current);
3477
3707
  }, []);
@@ -3503,7 +3733,9 @@ function TextArea(props) {
3503
3733
  class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
3504
3734
  onInput: handleLocalInput,
3505
3735
  onFocus: onFocus,
3736
+ onKeyDown: handleOnKeyDown,
3506
3737
  onBlur: handleOnBlur,
3738
+ onPaste: handleOnPaste,
3507
3739
  placeholder: placeholder,
3508
3740
  rows: rows,
3509
3741
  value: localValue,
@@ -3524,6 +3756,7 @@ function TextArea(props) {
3524
3756
  * @param {Function} props.setValue
3525
3757
  * @param {Function} props.onFocus
3526
3758
  * @param {Function} props.onBlur
3759
+ * @param {Function} props.onPaste
3527
3760
  * @param {number} props.rows
3528
3761
  * @param {boolean} props.monospace
3529
3762
  * @param {Function} [props.validate]
@@ -3544,6 +3777,7 @@ function TextAreaEntry(props) {
3544
3777
  validate,
3545
3778
  onFocus,
3546
3779
  onBlur,
3780
+ onPaste,
3547
3781
  placeholder,
3548
3782
  autoResize,
3549
3783
  tooltip
@@ -3579,6 +3813,7 @@ function TextAreaEntry(props) {
3579
3813
  onInput: onInput,
3580
3814
  onFocus: onFocus,
3581
3815
  onBlur: onBlur,
3816
+ onPaste: onPaste,
3582
3817
  rows: rows,
3583
3818
  debounce: debounce,
3584
3819
  monospace: monospace,
@@ -3616,6 +3851,7 @@ function Textfield(props) {
3616
3851
  onInput: commitValue,
3617
3852
  onFocus,
3618
3853
  onBlur,
3854
+ onPaste,
3619
3855
  placeholder,
3620
3856
  value = '',
3621
3857
  tooltip
@@ -3635,11 +3871,35 @@ function Textfield(props) {
3635
3871
  const trimmedValue = e.target.value.trim();
3636
3872
 
3637
3873
  // trim and commit on blur
3874
+ handleInput.cancel?.();
3638
3875
  onInput(trimmedValue);
3876
+ setLocalValue(trimmedValue);
3639
3877
  if (onBlur) {
3640
3878
  onBlur(e);
3641
3879
  }
3642
3880
  };
3881
+ const handleOnPaste = e => {
3882
+ const input = e.target;
3883
+ const isFieldEmpty = !input.value;
3884
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
3885
+
3886
+ // Trim and handle paste if field is empty or all content is selected (overwrite)
3887
+ if (isFieldEmpty || isAllSelected) {
3888
+ const trimmedValue = e.clipboardData.getData('text').trim();
3889
+ setLocalValue(trimmedValue);
3890
+ handleInput(trimmedValue);
3891
+ if (onPaste) {
3892
+ onPaste(e);
3893
+ }
3894
+ e.preventDefault();
3895
+ return;
3896
+ }
3897
+
3898
+ // Allow default paste behavior for normal text editing
3899
+ if (onPaste) {
3900
+ onPaste(e);
3901
+ }
3902
+ };
3643
3903
  const handleLocalInput = e => {
3644
3904
  if (e.target.value === localValue) {
3645
3905
  return;
@@ -3653,6 +3913,11 @@ function Textfield(props) {
3653
3913
  }
3654
3914
  setLocalValue(value);
3655
3915
  }, [value]);
3916
+ const handleOnKeyDown = e => {
3917
+ if (isCmdWithChar(e)) {
3918
+ handleInput.flush();
3919
+ }
3920
+ };
3656
3921
  return jsxs("div", {
3657
3922
  class: "bio-properties-panel-textfield",
3658
3923
  children: [jsx("label", {
@@ -3675,7 +3940,9 @@ function Textfield(props) {
3675
3940
  class: "bio-properties-panel-input",
3676
3941
  onInput: handleLocalInput,
3677
3942
  onFocus: onFocus,
3943
+ onKeyDown: handleOnKeyDown,
3678
3944
  onBlur: handleOnBlur,
3945
+ onPaste: handleOnPaste,
3679
3946
  placeholder: placeholder,
3680
3947
  value: localValue
3681
3948
  })]
@@ -3710,6 +3977,7 @@ function TextfieldEntry(props) {
3710
3977
  validate,
3711
3978
  onFocus,
3712
3979
  onBlur,
3980
+ onPaste,
3713
3981
  placeholder,
3714
3982
  tooltip
3715
3983
  } = props;
@@ -3745,6 +4013,7 @@ function TextfieldEntry(props) {
3745
4013
  onInput: onInput,
3746
4014
  onFocus: onFocus,
3747
4015
  onBlur: onBlur,
4016
+ onPaste: onPaste,
3748
4017
  placeholder: placeholder,
3749
4018
  value: value,
3750
4019
  tooltip: tooltip,
@@ -3777,7 +4046,7 @@ const DEFAULT_DEBOUNCE_TIME = 600;
3777
4046
  * - If `debounceDelay` is `false`, the function executes immediately without debouncing.
3778
4047
  * - If a number is provided, the function execution is delayed by the given time in milliseconds.
3779
4048
  *
3780
- * @param { Boolean | Number } [debounceDelay=300]
4049
+ * @param { Boolean | Number } [debounceDelay=600]
3781
4050
  *
3782
4051
  * @example
3783
4052
  * const debounce = debounceInput();
@@ -4097,6 +4366,24 @@ function cancel(event) {
4097
4366
  event.stopPropagation();
4098
4367
  }
4099
4368
 
4369
+ /**
4370
+ * @typedef {Object} FeelPopupProps
4371
+ * @property {string} entryId
4372
+ * @property {Function} onInput
4373
+ * @property {Function} onClose
4374
+ * @property {string} title
4375
+ * @property {'feel'|'feelers'} type
4376
+ * @property {string} value
4377
+ * @property {Array} [links]
4378
+ * @property {Array|Object} [variables]
4379
+ * @property {Object} [position]
4380
+ * @property {string} [hostLanguage]
4381
+ * @property {boolean} [singleLine]
4382
+ * @property {HTMLElement} [sourceElement]
4383
+ * @property {HTMLElement|string} [tooltipContainer]
4384
+ * @property {Object} [eventBus]
4385
+ */
4386
+
4100
4387
  const FEEL_POPUP_WIDTH = 700;
4101
4388
  const FEEL_POPUP_HEIGHT = 250;
4102
4389