@bpmn-io/properties-panel 3.33.1 → 3.34.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.js CHANGED
@@ -174,6 +174,20 @@ OpenPopupIcon.defaultProps = {
174
174
  viewBox: "0 0 16 16"
175
175
  };
176
176
 
177
+ /**
178
+ * @typedef { {
179
+ * getElementLabel: (element: object) => string,
180
+ * getTypeLabel: (element: object) => string,
181
+ * getElementIcon: (element: object) => import('preact').Component,
182
+ * getDocumentationRef: (element: object) => string
183
+ * } } HeaderProvider
184
+ */
185
+
186
+ /**
187
+ * @param {Object} props
188
+ * @param {Object} props.element,
189
+ * @param {HeaderProvider} props.headerProvider
190
+ */
177
191
  function Header(props) {
178
192
  const {
179
193
  element,
@@ -201,11 +215,9 @@ function Header(props) {
201
215
  }), jsxRuntime.jsxs("div", {
202
216
  class: "bio-properties-panel-header-labels",
203
217
  children: [jsxRuntime.jsx("div", {
204
- title: type,
205
218
  class: "bio-properties-panel-header-type",
206
219
  children: type
207
220
  }), label ? jsxRuntime.jsx("div", {
208
- title: label,
209
221
  class: "bio-properties-panel-header-label",
210
222
  children: label
211
223
  }) : null]
@@ -298,6 +310,26 @@ function useTooltipContext(id, element) {
298
310
  return getTooltipForId(id, element);
299
311
  }
300
312
 
313
+ /**
314
+ * @typedef {Object} TooltipProps
315
+ * @property {Object} [parent] - Parent element ref for portal rendering
316
+ * @property {String} [direction='right'] - Tooltip direction ( 'right', 'top')
317
+ * @property {String} [position] - Custom CSS position override
318
+ * @property {Number} [showDelay=250] - Delay in ms before showing tooltip on hover
319
+ * @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
320
+ * @property {*} [children] - Child elements to render inside the tooltip wrapper
321
+ */
322
+
323
+ /**
324
+ * Tooltip wrapper that provides context-based tooltip content lookup.
325
+ * All props are forwarded to the underlying Tooltip component.
326
+ *
327
+ * @param {TooltipProps & {
328
+ * forId: String,
329
+ * value?: String|Object,
330
+ * element?: Object
331
+ * }} props - Shared tooltip props plus wrapper-specific ones
332
+ */
301
333
  function TooltipWrapper(props) {
302
334
  const {
303
335
  forId,
@@ -314,35 +346,77 @@ function TooltipWrapper(props) {
314
346
  forId: `bio-properties-panel-${forId}`
315
347
  });
316
348
  }
349
+
350
+ /**
351
+ * @param {TooltipProps & {
352
+ * forId: String,
353
+ * value: String|Object
354
+ * }} props
355
+ */
317
356
  function Tooltip(props) {
318
357
  const {
319
358
  forId,
320
359
  value,
321
360
  parent,
322
361
  direction = 'right',
323
- position
362
+ position,
363
+ showDelay = 250,
364
+ hideDelay = 250
324
365
  } = props;
325
366
  const [visible, setVisible] = hooks.useState(false);
326
-
327
- // Tooltip will be shown after SHOW_DELAY ms from hovering over the source element.
328
- const SHOW_DELAY = 200;
329
- let timeout = null;
367
+ const showTimeoutRef = hooks.useRef(null);
368
+ const hideTimeoutRef = hooks.useRef(null);
330
369
  const wrapperRef = hooks.useRef(null);
331
370
  const tooltipRef = hooks.useRef(null);
332
371
  const show = (_, delay) => {
372
+ clearTimeout(showTimeoutRef.current);
373
+ clearTimeout(hideTimeoutRef.current);
333
374
  if (visible) return;
334
375
  if (delay) {
335
- timeout = setTimeout(() => {
376
+ showTimeoutRef.current = setTimeout(() => {
336
377
  setVisible(true);
337
- }, SHOW_DELAY);
378
+ }, showDelay);
338
379
  } else {
339
380
  setVisible(true);
340
381
  }
341
382
  };
342
- const hide = () => {
343
- clearTimeout(timeout);
344
- setVisible(false);
383
+ const handleWrapperMouseEnter = e => {
384
+ show(e, true);
385
+ };
386
+ const hide = (delay = false) => {
387
+ clearTimeout(showTimeoutRef.current);
388
+ clearTimeout(hideTimeoutRef.current);
389
+ if (delay) {
390
+ hideTimeoutRef.current = setTimeout(() => {
391
+ setVisible(false);
392
+ }, hideDelay);
393
+ } else {
394
+ setVisible(false);
395
+ }
345
396
  };
397
+
398
+ // Cleanup timeouts on unmount
399
+ hooks.useEffect(() => {
400
+ return () => {
401
+ clearTimeout(showTimeoutRef.current);
402
+ clearTimeout(hideTimeoutRef.current);
403
+ };
404
+ }, []);
405
+
406
+ // Handle click outside to close tooltip for non-focusable elements
407
+ hooks.useEffect(() => {
408
+ if (!visible) return;
409
+ const handleClickOutside = e => {
410
+ // If clicking outside both the wrapper and tooltip, hide it
411
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target) && tooltipRef.current && !tooltipRef.current.contains(e.target)) {
412
+ hide(false);
413
+ }
414
+ };
415
+ document.addEventListener('mousedown', handleClickOutside);
416
+ return () => {
417
+ document.removeEventListener('mousedown', handleClickOutside);
418
+ };
419
+ }, [visible, hide]);
346
420
  const handleMouseLeave = ({
347
421
  relatedTarget
348
422
  }) => {
@@ -350,23 +424,32 @@ function Tooltip(props) {
350
424
  if (relatedTarget === wrapperRef.current || relatedTarget === tooltipRef.current || relatedTarget?.parentElement === tooltipRef.current) {
351
425
  return;
352
426
  }
353
- hide();
427
+ const selection = window.getSelection();
428
+ if (selection && selection.toString().length > 0) {
429
+ // Check if selection is within tooltip content
430
+ const selectionRange = selection.getRangeAt(0);
431
+ if (tooltipRef.current?.contains(selectionRange.commonAncestorContainer) || tooltipRef.current?.contains(selection.anchorNode) || tooltipRef.current?.contains(selection.focusNode)) {
432
+ return; // Keep tooltip open during text selection
433
+ }
434
+ }
435
+ hide(true);
436
+ };
437
+ const handleTooltipMouseEnter = () => {
438
+ clearTimeout(hideTimeoutRef.current);
354
439
  };
355
440
  const handleFocusOut = e => {
356
441
  const {
357
- target
442
+ relatedTarget
358
443
  } = e;
359
444
 
360
- // Don't hide the tooltip if the wrapper or the tooltip itself is clicked.
361
- const isHovered = target.matches(':hover') || tooltipRef.current?.matches(':hover');
362
- if (target === wrapperRef.current && isHovered) {
363
- e.stopPropagation();
445
+ // Don't hide if focus moved to the tooltip or another element within the wrapper
446
+ if (tooltipRef.current?.contains(relatedTarget) || wrapperRef.current?.contains(relatedTarget)) {
364
447
  return;
365
448
  }
366
- hide();
449
+ hide(false);
367
450
  };
368
451
  const hideTooltipViaEscape = e => {
369
- e.code === 'Escape' && hide();
452
+ e.code === 'Escape' && hide(false);
370
453
  };
371
454
  const renderTooltip = () => {
372
455
  return jsxRuntime.jsxs("div", {
@@ -377,6 +460,7 @@ function Tooltip(props) {
377
460
  style: position || getTooltipPosition(wrapperRef.current),
378
461
  ref: tooltipRef,
379
462
  onClick: e => e.stopPropagation(),
463
+ onMouseEnter: handleTooltipMouseEnter,
380
464
  onMouseLeave: handleMouseLeave,
381
465
  children: [jsxRuntime.jsx("div", {
382
466
  class: "bio-properties-panel-tooltip-content",
@@ -390,7 +474,7 @@ function Tooltip(props) {
390
474
  class: "bio-properties-panel-tooltip-wrapper",
391
475
  tabIndex: "0",
392
476
  ref: wrapperRef,
393
- onMouseEnter: e => show(e, true),
477
+ onMouseEnter: handleWrapperMouseEnter,
394
478
  onMouseLeave: handleMouseLeave,
395
479
  onFocus: show,
396
480
  onBlur: handleFocusOut,
@@ -706,6 +790,9 @@ function useElementVisible(element) {
706
790
  return visible;
707
791
  }
708
792
 
793
+ /**
794
+ * @param {import('../PropertiesPanel').GroupDefinition} props
795
+ */
709
796
  function Group(props) {
710
797
  const {
711
798
  element,
@@ -760,8 +847,6 @@ function Group(props) {
760
847
  class: classnames('bio-properties-panel-group-header', edited ? '' : 'empty', open ? 'open' : '', sticky && open ? 'sticky' : ''),
761
848
  onClick: toggleOpen,
762
849
  children: [jsxRuntime.jsx("div", {
763
- title: props.tooltip ? null : label,
764
- "data-title": label,
765
850
  class: "bio-properties-panel-group-header-title",
766
851
  children: jsxRuntime.jsx(TooltipWrapper, {
767
852
  value: props.tooltip,
@@ -1114,6 +1199,13 @@ function useUpdateLayoutEffect(effect, deps) {
1114
1199
  }, deps);
1115
1200
  }
1116
1201
 
1202
+ /**
1203
+ *
1204
+ * @param {object} props
1205
+ * @param {string} [props.class]
1206
+ * @param {import('preact').Component[]} [props.menuItems]
1207
+ * @returns
1208
+ */
1117
1209
  function DropdownButton(props) {
1118
1210
  const {
1119
1211
  class: className,
@@ -1271,7 +1363,6 @@ function CollapsibleEntry(props) {
1271
1363
  class: "bio-properties-panel-collapsible-entry-header",
1272
1364
  onClick: toggleOpen,
1273
1365
  children: [jsxRuntime.jsx("div", {
1274
- title: label || placeholderLabel,
1275
1366
  class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
1276
1367
  children: label || placeholderLabel
1277
1368
  }), jsxRuntime.jsx("button", {
@@ -1308,6 +1399,9 @@ function CollapsibleEntry(props) {
1308
1399
  });
1309
1400
  }
1310
1401
 
1402
+ /**
1403
+ * @param {import('../PropertiesPanel').ListItemDefinition} props
1404
+ */
1311
1405
  function ListItem(props) {
1312
1406
  const {
1313
1407
  autoFocusEntry,
@@ -1410,8 +1504,6 @@ function ListGroup(props) {
1410
1504
  class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
1411
1505
  onClick: hasItems ? toggleOpen : noop$6,
1412
1506
  children: [jsxRuntime.jsx("div", {
1413
- title: props.tooltip ? null : label,
1414
- "data-title": label,
1415
1507
  class: "bio-properties-panel-group-header-title",
1416
1508
  children: jsxRuntime.jsx(TooltipWrapper, {
1417
1509
  value: props.tooltip,
@@ -1481,6 +1573,12 @@ function getNewItemIds(newItems, oldItems) {
1481
1573
  return newIds.filter(itemId => !oldIds.includes(itemId));
1482
1574
  }
1483
1575
 
1576
+ /**
1577
+ * @param {Object} props
1578
+ * @param {Object} props.element
1579
+ * @param {String} props.forId - id of the entry the description is used for
1580
+ * @param {String} props.value
1581
+ */
1484
1582
  function Description(props) {
1485
1583
  const {
1486
1584
  element,
@@ -1612,6 +1710,15 @@ function prefixId$8(id) {
1612
1710
  return `bio-properties-panel-${id}`;
1613
1711
  }
1614
1712
 
1713
+ /**
1714
+ * Button to open popups.
1715
+ *
1716
+ * @param {Object} props
1717
+ * @param {Function} props.onClick - Callback to trigger when the button is clicked.
1718
+ * @param {string} [props.title] - Tooltip text for the button.
1719
+ * @param {boolean} [props.disabled] - Whether the button is disabled.
1720
+ * @param {string} [props.className] - Additional class names for the button.
1721
+ */
1615
1722
  function OpenPopupButton({
1616
1723
  onClick,
1617
1724
  title = 'Open pop-up editor'
@@ -1757,6 +1864,7 @@ const FeelEditor = compat.forwardRef((props, ref) => {
1757
1864
  enableGutters,
1758
1865
  value,
1759
1866
  onInput,
1867
+ onKeyDown: onKeyDownProp = noop$4,
1760
1868
  onFeelToggle = noop$4,
1761
1869
  onLint = noop$4,
1762
1870
  onOpenPopup = noop$4,
@@ -1789,6 +1897,8 @@ const FeelEditor = compat.forwardRef((props, ref) => {
1789
1897
  * - AND the cursor is at the beginning of the input
1790
1898
  */
1791
1899
  const onKeyDown = e => {
1900
+ // Call parent onKeyDown handler first
1901
+ onKeyDownProp(e);
1792
1902
  if (e.key !== 'Backspace' || !editor) {
1793
1903
  return;
1794
1904
  }
@@ -1908,6 +2018,22 @@ function FeelIcon(props) {
1908
2018
  });
1909
2019
  }
1910
2020
 
2021
+ /**
2022
+ * @param {KeyboardEvent} event
2023
+ * @return {boolean}
2024
+ */
2025
+ function isCmd(event) {
2026
+ // ensure we don't react to AltGr
2027
+ // (mapped to CTRL + ALT)
2028
+ if (event.altKey) {
2029
+ return false;
2030
+ }
2031
+ return event.ctrlKey || event.metaKey;
2032
+ }
2033
+ function isCmdWithChar(event) {
2034
+ return isCmd(event) && event.key.length === 1 && /^[a-zA-Z]$/.test(event.key);
2035
+ }
2036
+
1911
2037
  function ToggleSwitch(props) {
1912
2038
  const {
1913
2039
  id,
@@ -2221,7 +2347,7 @@ function FeelTextfield(props) {
2221
2347
  element,
2222
2348
  label,
2223
2349
  hostLanguage,
2224
- onInput,
2350
+ onInput: commitValue,
2225
2351
  onBlur,
2226
2352
  onError,
2227
2353
  placeholder,
@@ -2237,6 +2363,12 @@ function FeelTextfield(props) {
2237
2363
  const [localValue, setLocalValue] = hooks.useState(value);
2238
2364
  const editorRef = useShowEntryEvent(id);
2239
2365
  const containerRef = hooks.useRef();
2366
+ const onInput = hooks.useCallback(newValue => {
2367
+ // we don't commit empty FEEL expressions,
2368
+ // but instead serialize them as <undefined>
2369
+ const newModelValue = newValue === '' || newValue === '=' ? undefined : newValue;
2370
+ commitValue(newModelValue);
2371
+ }, [commitValue]);
2240
2372
  const feelActive = minDash.isString(localValue) && localValue.startsWith('=') || feel === 'required';
2241
2373
  const feelOnlyValue = minDash.isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
2242
2374
  const feelLanguageContext = hooks.useContext(FeelLanguageContext);
@@ -2256,13 +2388,7 @@ function FeelTextfield(props) {
2256
2388
  /**
2257
2389
  * @type { import('min-dash').DebouncedFunction }
2258
2390
  */
2259
- const handleInputCallback = useDebounce(onInput, debounce);
2260
- const handleInput = newValue => {
2261
- // we don't commit empty FEEL expressions,
2262
- // but instead serialize them as <undefined>
2263
- const newModelValue = newValue === '' || newValue === '=' ? undefined : newValue;
2264
- handleInputCallback(newModelValue);
2265
- };
2391
+ const handleInput = useDebounce(onInput, debounce);
2266
2392
  const handleFeelToggle = useStaticCallback(() => {
2267
2393
  if (feel === 'required') {
2268
2394
  return;
@@ -2275,7 +2401,7 @@ function FeelTextfield(props) {
2275
2401
  handleInput(feelOnlyValue);
2276
2402
  }
2277
2403
  });
2278
- const handleLocalInput = newValue => {
2404
+ const handleLocalInput = (newValue, useDebounce = true) => {
2279
2405
  if (feelActive) {
2280
2406
  newValue = '=' + newValue;
2281
2407
  }
@@ -2283,23 +2409,33 @@ function FeelTextfield(props) {
2283
2409
  return;
2284
2410
  }
2285
2411
  setLocalValue(newValue);
2286
- handleInput(newValue);
2412
+ if (useDebounce) {
2413
+ handleInput(newValue);
2414
+ } else {
2415
+ onInput(newValue);
2416
+ }
2287
2417
  if (!feelActive && minDash.isString(newValue) && newValue.startsWith('=')) {
2288
2418
  // focus is behind `=` sign that will be removed
2289
2419
  setFocus(-1);
2290
2420
  }
2291
2421
  };
2292
2422
  const handleOnBlur = e => {
2423
+ handleInput.cancel?.();
2293
2424
  if (e.target.type === 'checkbox') {
2294
2425
  onInput(e.target.checked);
2295
2426
  } else {
2296
2427
  const trimmedValue = e.target.value.trim();
2297
- onInput(trimmedValue);
2428
+ handleLocalInput(trimmedValue, false);
2298
2429
  }
2299
2430
  if (onBlur) {
2300
2431
  onBlur(e);
2301
2432
  }
2302
2433
  };
2434
+ const handleOnKeyDown = e => {
2435
+ if (isCmdWithChar(e)) {
2436
+ handleInput.flush();
2437
+ }
2438
+ };
2303
2439
  const handleLint = useStaticCallback((lint = []) => {
2304
2440
  const syntaxError = lint.some(report => report.type === 'Syntax Error');
2305
2441
  if (syntaxError) {
@@ -2366,12 +2502,23 @@ function FeelTextfield(props) {
2366
2502
  if (feelActive || isPopupOpen) {
2367
2503
  return;
2368
2504
  }
2369
- const data = event.clipboardData.getData('application/FEEL');
2370
- if (data) {
2505
+ const feelData = event.clipboardData.getData('application/FEEL');
2506
+ if (feelData) {
2371
2507
  setTimeout(() => {
2372
2508
  handleFeelToggle();
2373
2509
  setFocus();
2374
2510
  });
2511
+ return;
2512
+ }
2513
+ const input = event.target;
2514
+ const isFieldEmpty = !input.value;
2515
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
2516
+ if (isFieldEmpty || isAllSelected) {
2517
+ const textData = event.clipboardData.getData('text');
2518
+ const trimmedValue = textData.trim();
2519
+ setLocalValue(trimmedValue);
2520
+ handleInput(trimmedValue);
2521
+ event.preventDefault();
2375
2522
  }
2376
2523
  };
2377
2524
  containerRef.current.addEventListener('copy', copyHandler);
@@ -2412,6 +2559,7 @@ function FeelTextfield(props) {
2412
2559
  }), feelActive ? jsxRuntime.jsx(FeelEditor, {
2413
2560
  name: id,
2414
2561
  onInput: handleLocalInput,
2562
+ onKeyDown: handleOnKeyDown,
2415
2563
  contentAttributes: {
2416
2564
  'id': prefixId$5(id),
2417
2565
  'aria-label': label
@@ -2434,6 +2582,7 @@ function FeelTextfield(props) {
2434
2582
  ...props,
2435
2583
  popupOpen: isPopupOpen,
2436
2584
  onInput: handleLocalInput,
2585
+ onKeyDown: handleOnKeyDown,
2437
2586
  onBlur: handleOnBlur,
2438
2587
  contentAttributes: {
2439
2588
  'id': prefixId$5(id),
@@ -2452,6 +2601,7 @@ const OptionalFeelInput = compat.forwardRef((props, ref) => {
2452
2601
  id,
2453
2602
  disabled,
2454
2603
  onInput,
2604
+ onKeyDown,
2455
2605
  value,
2456
2606
  onFocus,
2457
2607
  onBlur,
@@ -2487,6 +2637,7 @@ const OptionalFeelInput = compat.forwardRef((props, ref) => {
2487
2637
  class: "bio-properties-panel-input",
2488
2638
  onInput: e => onInput(e.target.value),
2489
2639
  onFocus: onFocus,
2640
+ onKeyDown: onKeyDown,
2490
2641
  onBlur: onBlur,
2491
2642
  placeholder: placeholder,
2492
2643
  value: value || ''
@@ -3081,6 +3232,22 @@ function prefixIdLabel(id) {
3081
3232
  return `bio-properties-panel-feelers-${id}-label`;
3082
3233
  }
3083
3234
 
3235
+ /**
3236
+ * Entry for handling lists represented as nested entries.
3237
+ *
3238
+ * @template Item
3239
+ * @param {object} props
3240
+ * @param {string} props.id
3241
+ * @param {*} props.element
3242
+ * @param {Function} props.onAdd
3243
+ * @param {import('preact').Component} props.component
3244
+ * @param {string} [props.label='<empty>']
3245
+ * @param {Function} [props.onRemove]
3246
+ * @param {Item[]} [props.items]
3247
+ * @param {boolean} [props.open]
3248
+ * @param {string|boolean} [props.autoFocusEntry] either a custom selector string or true to focus the first input
3249
+ * @returns
3250
+ */
3084
3251
  function List(props) {
3085
3252
  const {
3086
3253
  id,
@@ -3128,7 +3295,6 @@ function List(props) {
3128
3295
  class: classnames('bio-properties-panel-list-entry-header', sticky && open ? 'sticky' : ''),
3129
3296
  onClick: toggleOpen,
3130
3297
  children: [jsxRuntime.jsx("div", {
3131
- title: label,
3132
3298
  class: classnames('bio-properties-panel-list-entry-header-title', open && 'open'),
3133
3299
  children: label
3134
3300
  }), jsxRuntime.jsxs("div", {
@@ -3233,6 +3399,24 @@ function useNewItems(items = [], shouldReset) {
3233
3399
  return previousItems ? items.filter(item => !previousItems.includes(item)) : [];
3234
3400
  }
3235
3401
 
3402
+ /**
3403
+ * @typedef { { value: string, label: string, disabled: boolean, children: { value: string, label: string, disabled: boolean } } } Option
3404
+ */
3405
+
3406
+ /**
3407
+ * Provides basic select input.
3408
+ *
3409
+ * @param {object} props
3410
+ * @param {string} props.id
3411
+ * @param {string[]} props.path
3412
+ * @param {string} props.label
3413
+ * @param {Function} props.onChange
3414
+ * @param {Function} props.onFocus
3415
+ * @param {Function} props.onBlur
3416
+ * @param {Array<Option>} [props.options]
3417
+ * @param {string} props.value
3418
+ * @param {boolean} [props.disabled]
3419
+ */
3236
3420
  function Select(props) {
3237
3421
  const {
3238
3422
  id,
@@ -3387,6 +3571,17 @@ function prefixId$4(id) {
3387
3571
  return `bio-properties-panel-${id}`;
3388
3572
  }
3389
3573
 
3574
+ /**
3575
+ * @param {Object} props
3576
+ * @param {Function} props.debounce
3577
+ * @param {Boolean} [props.disabled]
3578
+ * @param {Object} props.element
3579
+ * @param {Function} props.getValue
3580
+ * @param {String} props.id
3581
+ * @param {Function} [props.onBlur]
3582
+ * @param {Function} [props.onFocus]
3583
+ * @param {Function} props.setValue
3584
+ */
3390
3585
  function Simple(props) {
3391
3586
  const {
3392
3587
  debounce,
@@ -3453,12 +3648,13 @@ function TextArea(props) {
3453
3648
  id,
3454
3649
  label,
3455
3650
  debounce,
3456
- onInput,
3651
+ onInput: commitValue,
3457
3652
  value = '',
3458
3653
  disabled,
3459
3654
  monospace,
3460
3655
  onFocus,
3461
3656
  onBlur,
3657
+ onPaste,
3462
3658
  autoResize = true,
3463
3659
  placeholder,
3464
3660
  rows = autoResize ? 1 : 2,
@@ -3466,16 +3662,16 @@ function TextArea(props) {
3466
3662
  } = props;
3467
3663
  const [localValue, setLocalValue] = hooks.useState(value);
3468
3664
  const ref = useShowEntryEvent(id);
3665
+ const onInput = hooks.useCallback(newValue => {
3666
+ const newModelValue = newValue === '' ? undefined : newValue;
3667
+ commitValue(newModelValue);
3668
+ }, [commitValue]);
3469
3669
  const visible = useElementVisible(ref.current);
3470
3670
 
3471
3671
  /**
3472
3672
  * @type { import('min-dash').DebouncedFunction }
3473
3673
  */
3474
- const handleInputCallback = useDebounce(onInput, debounce);
3475
- const handleInput = newValue => {
3476
- const newModelValue = newValue === '' ? undefined : newValue;
3477
- handleInputCallback(newModelValue);
3478
- };
3674
+ const handleInput = useDebounce(onInput, debounce);
3479
3675
  const handleLocalInput = e => {
3480
3676
  autoResize && resizeToContents(e.target);
3481
3677
  if (e.target.value === localValue) {
@@ -3488,11 +3684,40 @@ function TextArea(props) {
3488
3684
  const trimmedValue = e.target.value.trim();
3489
3685
 
3490
3686
  // trim and commit on blur
3687
+ handleInput.cancel?.();
3491
3688
  onInput(trimmedValue);
3689
+ setLocalValue(trimmedValue);
3492
3690
  if (onBlur) {
3493
3691
  onBlur(e);
3494
3692
  }
3495
3693
  };
3694
+ const handleOnPaste = e => {
3695
+ const input = e.target;
3696
+ const isFieldEmpty = !input.value;
3697
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
3698
+
3699
+ // Trim and handle paste if field is empty or all content is selected
3700
+ if (isFieldEmpty || isAllSelected) {
3701
+ const trimmedValue = e.clipboardData.getData('text').trim();
3702
+ setLocalValue(trimmedValue);
3703
+ handleInput(trimmedValue);
3704
+ if (onPaste) {
3705
+ onPaste(e);
3706
+ }
3707
+ e.preventDefault();
3708
+ return;
3709
+ }
3710
+
3711
+ // Allow default paste behavior for normal text editing
3712
+ if (onPaste) {
3713
+ onPaste(e);
3714
+ }
3715
+ };
3716
+ const handleOnKeyDown = e => {
3717
+ if (isCmdWithChar(e)) {
3718
+ handleInput.flush();
3719
+ }
3720
+ };
3496
3721
  hooks.useLayoutEffect(() => {
3497
3722
  autoResize && resizeToContents(ref.current);
3498
3723
  }, []);
@@ -3524,7 +3749,9 @@ function TextArea(props) {
3524
3749
  class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
3525
3750
  onInput: handleLocalInput,
3526
3751
  onFocus: onFocus,
3752
+ onKeyDown: handleOnKeyDown,
3527
3753
  onBlur: handleOnBlur,
3754
+ onPaste: handleOnPaste,
3528
3755
  placeholder: placeholder,
3529
3756
  rows: rows,
3530
3757
  value: localValue,
@@ -3545,6 +3772,7 @@ function TextArea(props) {
3545
3772
  * @param {Function} props.setValue
3546
3773
  * @param {Function} props.onFocus
3547
3774
  * @param {Function} props.onBlur
3775
+ * @param {Function} props.onPaste
3548
3776
  * @param {number} props.rows
3549
3777
  * @param {boolean} props.monospace
3550
3778
  * @param {Function} [props.validate]
@@ -3565,6 +3793,7 @@ function TextAreaEntry(props) {
3565
3793
  validate,
3566
3794
  onFocus,
3567
3795
  onBlur,
3796
+ onPaste,
3568
3797
  placeholder,
3569
3798
  autoResize,
3570
3799
  tooltip
@@ -3600,6 +3829,7 @@ function TextAreaEntry(props) {
3600
3829
  onInput: onInput,
3601
3830
  onFocus: onFocus,
3602
3831
  onBlur: onBlur,
3832
+ onPaste: onPaste,
3603
3833
  rows: rows,
3604
3834
  debounce: debounce,
3605
3835
  monospace: monospace,
@@ -3634,32 +3864,57 @@ function Textfield(props) {
3634
3864
  disabled = false,
3635
3865
  id,
3636
3866
  label,
3637
- onInput,
3867
+ onInput: commitValue,
3638
3868
  onFocus,
3639
3869
  onBlur,
3870
+ onPaste,
3640
3871
  placeholder,
3641
3872
  value = '',
3642
3873
  tooltip
3643
3874
  } = props;
3644
3875
  const [localValue, setLocalValue] = hooks.useState(value || '');
3645
3876
  const ref = useShowEntryEvent(id);
3877
+ const onInput = hooks.useCallback(newValue => {
3878
+ const newModelValue = newValue === '' ? undefined : newValue;
3879
+ commitValue(newModelValue);
3880
+ }, [commitValue]);
3646
3881
 
3647
3882
  /**
3648
3883
  * @type { import('min-dash').DebouncedFunction }
3649
3884
  */
3650
- const handleInputCallback = useDebounce(onInput, debounce);
3885
+ const handleInput = useDebounce(onInput, debounce);
3651
3886
  const handleOnBlur = e => {
3652
3887
  const trimmedValue = e.target.value.trim();
3653
3888
 
3654
3889
  // trim and commit on blur
3890
+ handleInput.cancel?.();
3655
3891
  onInput(trimmedValue);
3892
+ setLocalValue(trimmedValue);
3656
3893
  if (onBlur) {
3657
3894
  onBlur(e);
3658
3895
  }
3659
3896
  };
3660
- const handleInput = newValue => {
3661
- const newModelValue = newValue === '' ? undefined : newValue;
3662
- handleInputCallback(newModelValue);
3897
+ const handleOnPaste = e => {
3898
+ const input = e.target;
3899
+ const isFieldEmpty = !input.value;
3900
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === input.value.length;
3901
+
3902
+ // Trim and handle paste if field is empty or all content is selected (overwrite)
3903
+ if (isFieldEmpty || isAllSelected) {
3904
+ const trimmedValue = e.clipboardData.getData('text').trim();
3905
+ setLocalValue(trimmedValue);
3906
+ handleInput(trimmedValue);
3907
+ if (onPaste) {
3908
+ onPaste(e);
3909
+ }
3910
+ e.preventDefault();
3911
+ return;
3912
+ }
3913
+
3914
+ // Allow default paste behavior for normal text editing
3915
+ if (onPaste) {
3916
+ onPaste(e);
3917
+ }
3663
3918
  };
3664
3919
  const handleLocalInput = e => {
3665
3920
  if (e.target.value === localValue) {
@@ -3674,6 +3929,11 @@ function Textfield(props) {
3674
3929
  }
3675
3930
  setLocalValue(value);
3676
3931
  }, [value]);
3932
+ const handleOnKeyDown = e => {
3933
+ if (isCmdWithChar(e)) {
3934
+ handleInput.flush();
3935
+ }
3936
+ };
3677
3937
  return jsxRuntime.jsxs("div", {
3678
3938
  class: "bio-properties-panel-textfield",
3679
3939
  children: [jsxRuntime.jsx("label", {
@@ -3696,7 +3956,9 @@ function Textfield(props) {
3696
3956
  class: "bio-properties-panel-input",
3697
3957
  onInput: handleLocalInput,
3698
3958
  onFocus: onFocus,
3959
+ onKeyDown: handleOnKeyDown,
3699
3960
  onBlur: handleOnBlur,
3961
+ onPaste: handleOnPaste,
3700
3962
  placeholder: placeholder,
3701
3963
  value: localValue
3702
3964
  })]
@@ -3731,6 +3993,7 @@ function TextfieldEntry(props) {
3731
3993
  validate,
3732
3994
  onFocus,
3733
3995
  onBlur,
3996
+ onPaste,
3734
3997
  placeholder,
3735
3998
  tooltip
3736
3999
  } = props;
@@ -3766,6 +4029,7 @@ function TextfieldEntry(props) {
3766
4029
  onInput: onInput,
3767
4030
  onFocus: onFocus,
3768
4031
  onBlur: onBlur,
4032
+ onPaste: onPaste,
3769
4033
  placeholder: placeholder,
3770
4034
  value: value,
3771
4035
  tooltip: tooltip,
@@ -3798,7 +4062,7 @@ const DEFAULT_DEBOUNCE_TIME = 600;
3798
4062
  * - If `debounceDelay` is `false`, the function executes immediately without debouncing.
3799
4063
  * - If a number is provided, the function execution is delayed by the given time in milliseconds.
3800
4064
  *
3801
- * @param { Boolean | Number } [debounceDelay=300]
4065
+ * @param { Boolean | Number } [debounceDelay=600]
3802
4066
  *
3803
4067
  * @example
3804
4068
  * const debounce = debounceInput();
@@ -4118,6 +4382,24 @@ function cancel(event) {
4118
4382
  event.stopPropagation();
4119
4383
  }
4120
4384
 
4385
+ /**
4386
+ * @typedef {Object} FeelPopupProps
4387
+ * @property {string} entryId
4388
+ * @property {Function} onInput
4389
+ * @property {Function} onClose
4390
+ * @property {string} title
4391
+ * @property {'feel'|'feelers'} type
4392
+ * @property {string} value
4393
+ * @property {Array} [links]
4394
+ * @property {Array|Object} [variables]
4395
+ * @property {Object} [position]
4396
+ * @property {string} [hostLanguage]
4397
+ * @property {boolean} [singleLine]
4398
+ * @property {HTMLElement} [sourceElement]
4399
+ * @property {HTMLElement|string} [tooltipContainer]
4400
+ * @property {Object} [eventBus]
4401
+ */
4402
+
4121
4403
  const FEEL_POPUP_WIDTH = 700;
4122
4404
  const FEEL_POPUP_HEIGHT = 250;
4123
4405