@bpmn-io/form-js-editor 1.26.0 → 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -84,7 +84,7 @@ var TranslateModule = {
84
84
  };
85
85
 
86
86
  var FN_REF = '__fn';
87
- var DEFAULT_PRIORITY$4 = 1000;
87
+ var DEFAULT_PRIORITY$5 = 1000;
88
88
  var slice = Array.prototype.slice;
89
89
 
90
90
  /**
@@ -252,7 +252,7 @@ EventBus.prototype.on = function (events, priority, callback, that) {
252
252
  if (minDash.isFunction(priority)) {
253
253
  that = callback;
254
254
  callback = priority;
255
- priority = DEFAULT_PRIORITY$4;
255
+ priority = DEFAULT_PRIORITY$5;
256
256
  }
257
257
  if (!minDash.isNumber(priority)) {
258
258
  throw new Error('priority must be a number');
@@ -303,7 +303,7 @@ EventBus.prototype.once = function (events, priority, callback, that) {
303
303
  if (minDash.isFunction(priority)) {
304
304
  that = callback;
305
305
  callback = priority;
306
- priority = DEFAULT_PRIORITY$4;
306
+ priority = DEFAULT_PRIORITY$5;
307
307
  }
308
308
  if (!minDash.isNumber(priority)) {
309
309
  throw new Error('priority must be a number');
@@ -1385,7 +1385,7 @@ const ModularSection = props => {
1385
1385
  }) => RootElement ? jsxRuntime.jsx(RootElement, {
1386
1386
  children: children
1387
1387
  }) : jsxRuntime.jsx("div", {
1388
- className: rootClass,
1388
+ className: classnames(rootClass, 'bio-theme-parent'),
1389
1389
  children: children
1390
1390
  }), [rootClass, RootElement]);
1391
1391
  return shouldRender ? parent ? React.createPortal(jsxRuntime.jsx(Root, {
@@ -2096,7 +2096,8 @@ const EventContext = preact.createContext({
2096
2096
  * @typedef { {
2097
2097
  * parserDialect?: import('@bpmn-io/feel-editor').ParserDialect,
2098
2098
  * builtins?: import('@bpmn-io/feel-editor').Variable[],
2099
- * dialect?: import('@bpmn-io/feel-editor').Dialect
2099
+ * dialect?: import('@bpmn-io/feel-editor').Dialect,
2100
+ * engines?: Record<string, string>
2100
2101
  * } } FeelLanguageContextType
2101
2102
  */
2102
2103
 
@@ -2298,7 +2299,7 @@ function Tooltip(props) {
2298
2299
  const tooltipStyle = position || (tooltipPosition ? `right: ${tooltipPosition.right}; top: ${tooltipPosition.top}px;` : undefined);
2299
2300
  const arrowStyle = arrowOffset != null ? `margin-top: ${arrowOffset}px;` : undefined;
2300
2301
  return jsxRuntime.jsxs("div", {
2301
- class: `bio-properties-panel-tooltip ${direction}`,
2302
+ class: `bio-properties-panel-tooltip bio-theme-parent ${direction}`,
2302
2303
  role: "tooltip",
2303
2304
  id: "bio-properties-panel-tooltip",
2304
2305
  "aria-labelledby": forId,
@@ -2536,24 +2537,32 @@ function useShowEntryEvent(id) {
2536
2537
  * @param {boolean} value
2537
2538
  */
2538
2539
 
2540
+ /**
2541
+ * Resolve scroll container closest to the referenced element.
2542
+ */
2543
+ function getScrollContainer(ref, scrollContainerSelector) {
2544
+ return ref.current ? minDom.closest(ref.current, scrollContainerSelector, true) : null;
2545
+ }
2546
+
2539
2547
  /**
2540
2548
  * Use IntersectionObserver to identify when DOM element is in sticky mode.
2549
+ *
2541
2550
  * If sticky is observered setSticky(true) will be called.
2542
2551
  * If sticky mode is left, setSticky(false) will be called.
2543
2552
  *
2544
- *
2545
2553
  * @param {Object} ref
2546
2554
  * @param {string} scrollContainerSelector
2547
2555
  * @param {setSticky} setSticky
2548
2556
  */
2549
2557
  function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky) {
2550
- const [scrollContainer, setScrollContainer] = hooks.useState(minDom.query(scrollContainerSelector));
2558
+ // resolved from the observed element after mount (see effect below)
2559
+ const [scrollContainer, setScrollContainer] = hooks.useState(null);
2551
2560
  const updateScrollContainer = hooks.useCallback(() => {
2552
- const newScrollContainer = minDom.query(scrollContainerSelector);
2561
+ const newScrollContainer = getScrollContainer(ref, scrollContainerSelector);
2553
2562
  if (newScrollContainer !== scrollContainer) {
2554
2563
  setScrollContainer(newScrollContainer);
2555
2564
  }
2556
- }, [scrollContainerSelector, scrollContainer]);
2565
+ }, [ref, scrollContainerSelector, scrollContainer]);
2557
2566
  hooks.useEffect(() => {
2558
2567
  updateScrollContainer();
2559
2568
  }, [updateScrollContainer]);
@@ -2566,9 +2575,8 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
2566
2575
  if (!Observer) {
2567
2576
  return;
2568
2577
  }
2569
-
2570
- // TODO(@barmac): test this
2571
- if (!ref.current || !scrollContainer) {
2578
+ const element = ref.current;
2579
+ if (!element || !scrollContainer) {
2572
2580
  return;
2573
2581
  }
2574
2582
  const observer = new Observer(entries => {
@@ -2589,13 +2597,13 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
2589
2597
  // Use bottom margin to avoid stickyness when scrolling out to bottom
2590
2598
  threshold: [1]
2591
2599
  });
2592
- observer.observe(ref.current);
2600
+ observer.observe(element);
2593
2601
 
2594
2602
  // Unobserve if unmounted
2595
2603
  return () => {
2596
- observer.unobserve(ref.current);
2604
+ observer.unobserve(element);
2597
2605
  };
2598
- }, [ref.current, scrollContainer, setSticky]);
2606
+ }, [ref, scrollContainer, setSticky]);
2599
2607
  }
2600
2608
 
2601
2609
  /**
@@ -2622,23 +2630,6 @@ function useStaticCallback(callback) {
2622
2630
  });
2623
2631
  return hooks.useCallback((...args) => callbackRef.current(...args), []);
2624
2632
  }
2625
- function useElementVisible(element) {
2626
- const [visible, setVisible] = hooks.useState(!!element && !!element.clientHeight);
2627
- hooks.useLayoutEffect(() => {
2628
- if (!element) return;
2629
- const resizeObserver = new ResizeObserver(([entry]) => {
2630
- requestAnimationFrame(() => {
2631
- const newVisible = !!entry.contentRect.height;
2632
- if (newVisible !== visible) {
2633
- setVisible(newVisible);
2634
- }
2635
- });
2636
- });
2637
- resizeObserver.observe(element);
2638
- return () => resizeObserver.disconnect();
2639
- }, [element, visible]);
2640
- return visible;
2641
- }
2642
2633
 
2643
2634
  /**
2644
2635
  * @param {import('../PropertiesPanel').GroupDefinition} props
@@ -2990,7 +2981,7 @@ function PropertiesPanel$1(props) {
2990
2981
  children: jsxRuntime.jsx(EventContext.Provider, {
2991
2982
  value: eventContext,
2992
2983
  children: jsxRuntime.jsxs("div", {
2993
- class: "bio-properties-panel",
2984
+ class: "bio-properties-panel bio-theme-parent",
2994
2985
  children: [headerProvider ? jsxRuntime.jsx(Header, {
2995
2986
  element: element,
2996
2987
  headerProvider: headerProvider
@@ -3081,16 +3072,17 @@ function CollapsibleEntry(props) {
3081
3072
  }, [onShow, setOpen])
3082
3073
  };
3083
3074
  const placeholderLabel = translate('<empty>');
3075
+ const hasEntries = entries.length > 0;
3084
3076
  return jsxRuntime.jsxs("div", {
3085
3077
  "data-entry-id": id,
3086
3078
  class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
3087
3079
  children: [jsxRuntime.jsxs("div", {
3088
3080
  class: "bio-properties-panel-collapsible-entry-header",
3089
- onClick: toggleOpen,
3081
+ onClick: hasEntries ? toggleOpen : null,
3090
3082
  children: [jsxRuntime.jsx("div", {
3091
3083
  class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
3092
3084
  children: label || placeholderLabel
3093
- }), jsxRuntime.jsx("button", {
3085
+ }), hasEntries && jsxRuntime.jsx("button", {
3094
3086
  type: "button",
3095
3087
  title: translate('Toggle list item'),
3096
3088
  class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
@@ -3606,7 +3598,8 @@ const FeelEditor = React.forwardRef((props, ref) => {
3606
3598
  const {
3607
3599
  builtins,
3608
3600
  dialect,
3609
- parserDialect
3601
+ parserDialect,
3602
+ engines
3610
3603
  } = feelLanguageContext || {};
3611
3604
  useBufferedFocus(editor, ref);
3612
3605
  const handleInput = useStaticCallback(newValue => {
@@ -3649,6 +3642,7 @@ const FeelEditor = React.forwardRef((props, ref) => {
3649
3642
  builtins,
3650
3643
  dialect,
3651
3644
  parserDialect,
3645
+ engines,
3652
3646
  extensions: [...(enableGutters ? [view.lineNumbers()] : []), view.EditorView.lineWrapping, view.EditorView.domEventHandlers({
3653
3647
  blur: handleBlur
3654
3648
  })],
@@ -3683,6 +3677,12 @@ const FeelEditor = React.forwardRef((props, ref) => {
3683
3677
  }
3684
3678
  editor.setPlaceholder(placeholder);
3685
3679
  }, [placeholder]);
3680
+ hooks.useEffect(() => {
3681
+ if (!editor) {
3682
+ return;
3683
+ }
3684
+ editor.setEngines(engines);
3685
+ }, [engines]);
3686
3686
  const handleClick = () => {
3687
3687
  ref.current.focus();
3688
3688
  };
@@ -5004,16 +5004,10 @@ function isEdited$3(node) {
5004
5004
  function prefixId$4(id) {
5005
5005
  return `bio-properties-panel-${id}`;
5006
5006
  }
5007
- function resizeToContents(element) {
5008
- element.style.height = 'auto';
5009
-
5010
- // a 2px pixel offset is required to prevent scrollbar from
5011
- // appearing on OS with a full length scroll bar (Windows/Linux)
5012
- element.style.height = `${element.scrollHeight + 2}px`;
5013
- }
5014
5007
  function TextArea(props) {
5015
5008
  const {
5016
5009
  id,
5010
+ element,
5017
5011
  label,
5018
5012
  debounce,
5019
5013
  onInput: commitValue,
@@ -5026,22 +5020,31 @@ function TextArea(props) {
5026
5020
  autoResize = true,
5027
5021
  placeholder,
5028
5022
  rows = autoResize ? 1 : 2,
5029
- tooltip
5023
+ tooltip,
5024
+ translate = translateFallback
5030
5025
  } = props;
5031
5026
  const [localValue, setLocalValue] = hooks.useState(value);
5032
5027
  const ref = useShowEntryEvent(id);
5028
+ const containerRef = hooks.useRef();
5029
+
5030
+ // keep a live reference to the current value so callbacks captured by the
5031
+ // popup (frozen at open time) can compare against the latest value
5032
+ const localValueRef = hooks.useRef(localValue);
5033
+ localValueRef.current = localValue;
5034
+ const {
5035
+ eventBus
5036
+ } = hooks.useContext(EventContext);
5037
+ const [isPopupOpen, setIsPopupOpen] = hooks.useState(false);
5033
5038
  const onInput = hooks.useCallback(newValue => {
5034
5039
  const newModelValue = newValue === '' ? undefined : newValue;
5035
5040
  commitValue(newModelValue);
5036
5041
  }, [commitValue]);
5037
- const visible = useElementVisible(ref.current);
5038
5042
 
5039
5043
  /**
5040
5044
  * @type { import('min-dash').DebouncedFunction }
5041
5045
  */
5042
5046
  const handleInput = useDebounce(onInput, debounce);
5043
5047
  const handleLocalInput = e => {
5044
- autoResize && resizeToContents(e.target);
5045
5048
  if (e.target.value === localValue) {
5046
5049
  return;
5047
5050
  }
@@ -5086,12 +5089,35 @@ function TextArea(props) {
5086
5089
  handleInput.flush?.();
5087
5090
  }
5088
5091
  };
5089
- hooks.useLayoutEffect(() => {
5090
- autoResize && resizeToContents(ref.current);
5092
+ const handlePopupInput = newValue => {
5093
+ if (newValue === localValueRef.current) {
5094
+ return;
5095
+ }
5096
+ setLocalValue(newValue);
5097
+ handleInput(newValue);
5098
+ };
5099
+ const handleOpenPopup = () => {
5100
+ const isOpen = eventBus.fire('propertiesPanel.openPopup', {
5101
+ element,
5102
+ entryId: id,
5103
+ label,
5104
+ onInput: handlePopupInput,
5105
+ sourceElement: ref.current,
5106
+ value: localValue
5107
+ });
5108
+ if (isOpen) {
5109
+ eventBus.once('propertiesPanelPopup.close', () => {
5110
+ handleInput.flush?.();
5111
+ setIsPopupOpen(false);
5112
+ });
5113
+ }
5114
+ setIsPopupOpen(isOpen === true);
5115
+ };
5116
+ hooks.useEffect(() => {
5117
+ return () => {
5118
+ eventBus && eventBus.fire('propertiesPanel.closePopup');
5119
+ };
5091
5120
  }, []);
5092
- hooks.useLayoutEffect(() => {
5093
- visible && autoResize && resizeToContents(ref.current);
5094
- }, [visible]);
5095
5121
  hooks.useEffect(() => {
5096
5122
  if (value === localValue) {
5097
5123
  return;
@@ -5106,25 +5132,35 @@ function TextArea(props) {
5106
5132
  children: jsxRuntime.jsx(TooltipWrapper, {
5107
5133
  value: tooltip,
5108
5134
  forId: id,
5109
- element: props.element,
5135
+ element: element,
5110
5136
  children: label
5111
5137
  })
5112
- }), jsxRuntime.jsx("textarea", {
5113
- ref: ref,
5114
- id: prefixId$2(id),
5115
- name: id,
5116
- spellCheck: "false",
5117
- class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
5118
- onInput: handleLocalInput,
5119
- onFocus: onFocus,
5120
- onKeyDown: handleOnKeyDown,
5121
- onBlur: handleOnBlur,
5122
- onPaste: handleOnPaste,
5123
- placeholder: placeholder,
5124
- rows: rows,
5125
- value: localValue,
5126
- disabled: disabled,
5127
- "data-gramm": "false"
5138
+ }), jsxRuntime.jsxs("div", {
5139
+ class: classnames('bio-properties-panel-textarea-container', isPopupOpen ? 'popupOpen' : null),
5140
+ ref: containerRef,
5141
+ children: [isPopupOpen && jsxRuntime.jsx("div", {
5142
+ class: "bio-properties-panel-textarea__open-popup-placeholder",
5143
+ children: translate('Opened in editor')
5144
+ }), jsxRuntime.jsx("textarea", {
5145
+ ref: ref,
5146
+ id: prefixId$2(id),
5147
+ name: id,
5148
+ spellCheck: "false",
5149
+ class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
5150
+ onInput: handleLocalInput,
5151
+ onFocus: onFocus,
5152
+ onKeyDown: handleOnKeyDown,
5153
+ onBlur: handleOnBlur,
5154
+ onPaste: handleOnPaste,
5155
+ placeholder: placeholder,
5156
+ rows: rows,
5157
+ value: localValue,
5158
+ disabled: disabled,
5159
+ "data-gramm": "false"
5160
+ }), eventBus && !disabled && jsxRuntime.jsx(OpenPopupButton, {
5161
+ onClick: handleOpenPopup,
5162
+ translate: translate
5163
+ })]
5128
5164
  })]
5129
5165
  });
5130
5166
  }
@@ -5145,6 +5181,7 @@ function TextArea(props) {
5145
5181
  * @param {boolean} props.monospace
5146
5182
  * @param {Function} [props.validate]
5147
5183
  * @param {boolean} [props.disabled]
5184
+ * @param {Function} [props.translate]
5148
5185
  */
5149
5186
  function TextAreaEntry(props) {
5150
5187
  const {
@@ -5164,7 +5201,8 @@ function TextAreaEntry(props) {
5164
5201
  onPaste,
5165
5202
  placeholder,
5166
5203
  autoResize,
5167
- tooltip
5204
+ tooltip,
5205
+ translate
5168
5206
  } = props;
5169
5207
  const globalError = useError(id);
5170
5208
  const [localError, setLocalError] = hooks.useState(null);
@@ -5205,6 +5243,7 @@ function TextAreaEntry(props) {
5205
5243
  placeholder: placeholder,
5206
5244
  autoResize: autoResize,
5207
5245
  tooltip: tooltip,
5246
+ translate: translate,
5208
5247
  element: element
5209
5248
  }, element), error && jsxRuntime.jsx("div", {
5210
5249
  class: "bio-properties-panel-error",
@@ -5420,6 +5459,16 @@ function isEdited(node) {
5420
5459
  function prefixId$1(id) {
5421
5460
  return `bio-properties-panel-${id}`;
5422
5461
  }
5462
+ function getPopupTitle({
5463
+ element,
5464
+ label
5465
+ }) {
5466
+ let popupTitle = '';
5467
+ if (element && element.type) {
5468
+ popupTitle = `${element.type} / `;
5469
+ }
5470
+ return `${popupTitle}${label}`;
5471
+ }
5423
5472
 
5424
5473
  /**
5425
5474
  * Add a dragger that calls back the passed function with
@@ -5504,6 +5553,9 @@ const noop = () => {};
5504
5553
  * @param {Function} [props.onPostDeactivate]
5505
5554
  * @param {boolean} [props.returnFocus]
5506
5555
  * @param {boolean} [props.closeOnEscape]
5556
+ * @param {(event: KeyboardEvent) => boolean} [props.allowFocusMove] -
5557
+ * Whether a Tab keypress may move focus. Return false to leave the keypress
5558
+ * to the focused element, e.g. an editor navigating snippet placeholders.
5507
5559
  * @param {string} props.title
5508
5560
  * @param {Ref} [ref]
5509
5561
  */
@@ -5519,6 +5571,7 @@ function PopupComponent(props, globalRef) {
5519
5571
  onPostDeactivate = noop,
5520
5572
  returnFocus = true,
5521
5573
  closeOnEscape = true,
5574
+ allowFocusMove = () => true,
5522
5575
  title
5523
5576
  } = props;
5524
5577
  const focusTrapRef = hooks.useRef(null);
@@ -5538,13 +5591,14 @@ function PopupComponent(props, globalRef) {
5538
5591
  focusTrapRef.current.activate();
5539
5592
  }
5540
5593
  };
5541
- let style = {};
5542
- if (position) {
5543
- style = {
5544
- ...style,
5545
- top: position.top + 'px',
5546
- left: position.left + 'px'
5547
- };
5594
+ const {
5595
+ top,
5596
+ left
5597
+ } = position || getCenterPosition(width, height);
5598
+ const style = {};
5599
+ if (minDash.isDefined(top) && minDash.isDefined(left)) {
5600
+ style.top = top + 'px';
5601
+ style.left = left + 'px';
5548
5602
  }
5549
5603
  if (width) {
5550
5604
  style.width = width + 'px';
@@ -5568,6 +5622,8 @@ function PopupComponent(props, globalRef) {
5568
5622
  clickOutsideDeactivates: true,
5569
5623
  delayInitialFocus,
5570
5624
  fallbackFocus: popupRef.current,
5625
+ isKeyForward: event => isTab(event) && !event.shiftKey && allowFocusMove(event),
5626
+ isKeyBackward: event => isTab(event) && event.shiftKey && allowFocusMove(event),
5571
5627
  onPostActivate,
5572
5628
  onPostDeactivate,
5573
5629
  returnFocusOnDeactivate: returnFocus
@@ -5582,21 +5638,23 @@ function PopupComponent(props, globalRef) {
5582
5638
  ref: popupRef,
5583
5639
  onKeyDown: handleKeydown,
5584
5640
  role: "dialog",
5585
- class: classnames('bio-properties-panel-popup', className),
5641
+ class: classnames('bio-properties-panel-popup', 'bio-theme-parent', className),
5586
5642
  style: style,
5587
5643
  children: props.children
5588
5644
  });
5589
5645
  }
5590
- const Popup = React.forwardRef(PopupComponent);
5591
- Popup.Title = Title;
5592
- Popup.Body = Body;
5593
- Popup.Footer = Footer;
5646
+ const Popup$1 = React.forwardRef(PopupComponent);
5647
+ Popup$1.Title = Title;
5648
+ Popup$1.Body = Body;
5649
+ Popup$1.Footer = Footer;
5594
5650
  function Title(props) {
5651
+ const {
5652
+ eventBus
5653
+ } = hooks.useContext(EventContext);
5595
5654
  const {
5596
5655
  children,
5597
5656
  className,
5598
5657
  draggable,
5599
- eventBus,
5600
5658
  title,
5601
5659
  showCloseButton = false,
5602
5660
  closeButtonTooltip = 'Close popup',
@@ -5701,6 +5759,26 @@ function Footer(props) {
5701
5759
 
5702
5760
  // helpers //////////////////////
5703
5761
 
5762
+ /**
5763
+ * Center the popup in the viewport, based on its own dimensions.
5764
+ *
5765
+ * @param {number} [width]
5766
+ * @param {number} [height]
5767
+ */
5768
+ function getCenterPosition(width, height) {
5769
+ if (!width || !height) {
5770
+ return {};
5771
+ }
5772
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
5773
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
5774
+ return {
5775
+ left: Math.max(0, (viewportWidth - width) / 2),
5776
+ top: Math.max(0, (viewportHeight - height) / 2)
5777
+ };
5778
+ }
5779
+ function isTab(event) {
5780
+ return event.key === 'Tab';
5781
+ }
5704
5782
  function getPopupParent(node) {
5705
5783
  return node.closest('.bio-properties-panel-popup');
5706
5784
  }
@@ -5719,22 +5797,20 @@ function cancel(event) {
5719
5797
  * @property {string} value
5720
5798
  * @property {Array} [links]
5721
5799
  * @property {Array|Object} [variables]
5722
- * @property {Object} [position]
5723
5800
  * @property {string} [hostLanguage]
5724
5801
  * @property {boolean} [singleLine]
5725
5802
  * @property {HTMLElement} [sourceElement]
5726
5803
  * @property {HTMLElement|string} [tooltipContainer]
5727
- * @property {Object} [eventBus]
5728
5804
  */
5729
5805
 
5730
5806
  const FEEL_POPUP_WIDTH = 700;
5731
- const FEEL_POPUP_HEIGHT = 250;
5807
+ const FEEL_POPUP_HEIGHT = 400;
5732
5808
 
5733
5809
  /**
5734
5810
  * FEEL expression editor popup component
5735
5811
  * @param {FeelPopupProps} props
5736
5812
  */
5737
- function FeelPopup$1(props) {
5813
+ function FeelPopup(props) {
5738
5814
  const {
5739
5815
  entryId,
5740
5816
  onInput,
@@ -5744,12 +5820,10 @@ function FeelPopup$1(props) {
5744
5820
  value,
5745
5821
  links,
5746
5822
  variables,
5747
- position,
5748
5823
  hostLanguage,
5749
5824
  singleLine,
5750
5825
  sourceElement,
5751
5826
  tooltipContainer,
5752
- eventBus,
5753
5827
  feelLanguageContext
5754
5828
  } = props;
5755
5829
  const editorRef = hooks.useRef();
@@ -5779,20 +5853,19 @@ function FeelPopup$1(props) {
5779
5853
  editorRef.current.focus();
5780
5854
  }
5781
5855
  }, [editorRef]);
5782
- return jsxRuntime.jsxs(Popup, {
5856
+ return jsxRuntime.jsxs(Popup$1, {
5783
5857
  className: "bio-properties-panel-feel-popup",
5784
- position: position,
5785
5858
  title: title,
5786
5859
  returnFocus: false,
5787
5860
  closeOnEscape: false,
5788
5861
  delayInitialFocus: false,
5862
+ allowFocusMove: event => !isSnippetNavigation(event),
5789
5863
  onPostDeactivate: handleSetReturnFocus,
5790
5864
  height: FEEL_POPUP_HEIGHT,
5791
5865
  width: FEEL_POPUP_WIDTH,
5792
5866
  ref: popupRef,
5793
- children: [jsxRuntime.jsx(Popup.Title, {
5867
+ children: [jsxRuntime.jsx(Popup$1.Title, {
5794
5868
  title: title,
5795
- eventBus: eventBus,
5796
5869
  showCloseButton: true,
5797
5870
  closeButtonTooltip: "Save and close",
5798
5871
  onClose: onClose,
@@ -5808,14 +5881,13 @@ function FeelPopup$1(props) {
5808
5881
  }, index);
5809
5882
  })
5810
5883
  })
5811
- }), jsxRuntime.jsx(Popup.Body, {
5884
+ }), jsxRuntime.jsx(Popup$1.Body, {
5812
5885
  children: jsxRuntime.jsxs("div", {
5813
5886
  onKeyDownCapture: onKeyDownCapture,
5814
5887
  onKeyDown: onKeyDown,
5815
5888
  class: "bio-properties-panel-feel-popup__body",
5816
5889
  children: [type === 'feel' && jsxRuntime.jsx(FeelEditor, {
5817
5890
  enableGutters: true,
5818
- id: prefixId(entryId),
5819
5891
  name: entryId,
5820
5892
  onInput: onInput,
5821
5893
  value: value,
@@ -5824,7 +5896,6 @@ function FeelPopup$1(props) {
5824
5896
  ref: editorRef,
5825
5897
  tooltipContainer: tooltipContainer
5826
5898
  }), type === 'feelers' && jsxRuntime.jsx(TemplatingEditor, {
5827
- id: prefixId(entryId),
5828
5899
  contentAttributes: {
5829
5900
  'aria-label': title
5830
5901
  },
@@ -5841,34 +5912,107 @@ function FeelPopup$1(props) {
5841
5912
  })]
5842
5913
  });
5843
5914
  }
5844
- function prefixId(id) {
5845
- return `bio-properties-panel-${id}`;
5846
- }
5847
5915
  function autoCompletionOpen(element) {
5848
5916
  const editor = element.closest('.cm-editor');
5849
5917
  return editor ? editor.querySelector('.cm-tooltip-autocomplete') : null;
5850
5918
  }
5851
- function getPopupTitle({
5852
- element,
5853
- label
5854
- }) {
5855
- let popupTitle = '';
5856
- if (element && element.type) {
5857
- popupTitle = `${element.type} / `;
5858
- }
5859
- return `${popupTitle}${label}`;
5919
+
5920
+ // while a snippet is active, Tab navigates its placeholders inside the editor
5921
+ function isSnippetNavigation(event) {
5922
+ const editor = event.target.closest('.cm-editor');
5923
+ return !!(editor && editor.querySelector('.cm-snippetField'));
5860
5924
  }
5861
- function getPopupPosition() {
5862
- const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
5863
- const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
5864
- return {
5865
- left: Math.max(0, (viewportWidth - FEEL_POPUP_WIDTH) / 2),
5866
- top: Math.max(0, (viewportHeight - FEEL_POPUP_HEIGHT) / 2)
5925
+
5926
+ /**
5927
+ * @typedef {Object} TextPopupProps
5928
+ * @property {string} entryId
5929
+ * @property {Function} onInput
5930
+ * @property {Function} onClose
5931
+ * @property {string} title
5932
+ * @property {string} value
5933
+ * @property {HTMLElement} [sourceElement]
5934
+ */
5935
+
5936
+ const TEXT_POPUP_WIDTH = 700;
5937
+ const TEXT_POPUP_HEIGHT = 400;
5938
+
5939
+ /**
5940
+ * Plain text editor popup component.
5941
+ *
5942
+ * Intentionally kept separate from the FEEL popup: the two are expected to
5943
+ * diverge as we add FEEL-specific capabilities that do not apply to plain text.
5944
+ *
5945
+ * @param {TextPopupProps} props
5946
+ */
5947
+ function TextPopup(props) {
5948
+ const {
5949
+ entryId,
5950
+ onInput,
5951
+ onClose,
5952
+ title,
5953
+ value,
5954
+ sourceElement
5955
+ } = props;
5956
+ const editorRef = hooks.useRef();
5957
+ const popupRef = hooks.useRef();
5958
+ const handleSetReturnFocus = () => {
5959
+ sourceElement && sourceElement.focus();
5867
5960
  };
5961
+ hooks.useEffect(() => {
5962
+ // set focus on editor when popup is opened
5963
+ if (editorRef.current) {
5964
+ editorRef.current.focus();
5965
+ }
5966
+ }, [editorRef]);
5967
+ return jsxRuntime.jsxs(Popup$1, {
5968
+ className: "bio-properties-panel-text-popup",
5969
+ title: title,
5970
+ returnFocus: false,
5971
+ closeOnEscape: true,
5972
+ delayInitialFocus: false,
5973
+ onClose: onClose,
5974
+ onPostDeactivate: handleSetReturnFocus,
5975
+ height: TEXT_POPUP_HEIGHT,
5976
+ width: TEXT_POPUP_WIDTH,
5977
+ ref: popupRef,
5978
+ children: [jsxRuntime.jsx(Popup$1.Title, {
5979
+ title: title,
5980
+ showCloseButton: true,
5981
+ closeButtonTooltip: "Save and close",
5982
+ onClose: onClose,
5983
+ draggable: true
5984
+ }), jsxRuntime.jsx(Popup$1.Body, {
5985
+ children: jsxRuntime.jsx("textarea", {
5986
+ id: prefixId(entryId),
5987
+ name: entryId,
5988
+ class: "bio-properties-panel-input",
5989
+ ref: editorRef,
5990
+ onInput: e => onInput(e.target.value),
5991
+ value: value || '',
5992
+ spellCheck: "false",
5993
+ autoComplete: "off",
5994
+ "aria-label": title,
5995
+ "data-gramm": "false"
5996
+ })
5997
+ })]
5998
+ });
5999
+ }
6000
+
6001
+ // The in-panel field stays mounted (hidden) while the popup is open, so we
6002
+ // scope the popup editor id to keep it unique in the DOM.
6003
+ function prefixId(id) {
6004
+ return `bio-properties-panel-popup-${id}`;
5868
6005
  }
6006
+ const DEFAULT_POPUP_TYPE = 'text';
6007
+
6008
+ // consumers registering via #registerProvider default to DEFAULT_PRIORITY,
6009
+ // so their providers take precedence over the LOW_PRIORITY built-ins.
6010
+ const DEFAULT_PRIORITY$4 = 1000;
6011
+ const LOW_PRIORITY$2 = 500;
5869
6012
 
5870
6013
  /**
5871
- * FEEL popup component, built as a singleton.
6014
+ * Popup manager, built as a singleton. Renders the registered provider for a
6015
+ * given popup type; consumers may plug in their own via #registerProvider.
5872
6016
  *
5873
6017
  * In order to implement a custom replacement, handle the following events:
5874
6018
  * - `propertiesPanel.openPopup`
@@ -5881,16 +6025,19 @@ function getPopupPosition() {
5881
6025
  * - `feelPopup.close` - fired before the popup is unmounted. Event context contains the DOM node of the popup as `domNode`
5882
6026
  * - `feelPopup.closed` - fired after the popup is unmounted
5883
6027
  */
5884
- class FeelPopup {
6028
+ class Popup {
5885
6029
  constructor(eventBus, config = {}) {
5886
6030
  this._eventBus = eventBus;
5887
6031
  this._config = config;
5888
6032
  this._isOpen = false;
5889
- eventBus.on('propertiesPanel.openPopup', (_, context) => {
5890
- this.open(context.entryId, context, context.sourceElement);
5891
6033
 
5892
- // return true to indicate that popup was opened
5893
- return true;
6034
+ // built-in providers, registered at LOW_PRIORITY so consumers can
6035
+ // override them via #registerProvider using the default priority
6036
+ this.registerProvider('feel', LOW_PRIORITY$2, FeelPopup);
6037
+ this.registerProvider('feelers', LOW_PRIORITY$2, FeelPopup);
6038
+ this.registerProvider('text', LOW_PRIORITY$2, TextPopup);
6039
+ eventBus.on('propertiesPanel.openPopup', (_, context) => {
6040
+ return this.open(context.entryId, context, context.sourceElement);
5894
6041
  });
5895
6042
  eventBus.on(['propertiesPanel.closePopup', 'propertiesPanel.detach'], () => {
5896
6043
  this.close();
@@ -5898,7 +6045,43 @@ class FeelPopup {
5898
6045
  }
5899
6046
 
5900
6047
  /**
5901
- * Check if the FEEL popup is open.
6048
+ * Register a popup provider (component) for a given type.
6049
+ *
6050
+ * A higher `priority` wins when multiple providers are registered for the
6051
+ * same type; the built-in providers use a low priority so consumers override
6052
+ * them by default.
6053
+ *
6054
+ * @param {string} type
6055
+ * @param {number} [priority=DEFAULT_PRIORITY]
6056
+ * @param {Function|import('preact').Component} provider
6057
+ */
6058
+ registerProvider(type, priority, provider) {
6059
+ if (!provider) {
6060
+ provider = priority;
6061
+ priority = DEFAULT_PRIORITY$4;
6062
+ }
6063
+ this._eventBus.on('propertiesPanelPopup.getProviders.' + type, priority, function (event) {
6064
+ event.providers.push(provider);
6065
+ });
6066
+ }
6067
+
6068
+ /**
6069
+ * Get the popup providers registered for a type.
6070
+ *
6071
+ * @param {string} type
6072
+ * @return {Array<Function|import('preact').Component>}
6073
+ */
6074
+ _getProviders(type) {
6075
+ const event = this._eventBus.createEvent({
6076
+ type: 'propertiesPanelPopup.getProviders.' + type,
6077
+ providers: []
6078
+ });
6079
+ this._eventBus.fire(event);
6080
+ return event.providers;
6081
+ }
6082
+
6083
+ /**
6084
+ * Check if the popup is open.
5902
6085
  * @return {Boolean}
5903
6086
  */
5904
6087
  isOpen() {
@@ -5906,7 +6089,7 @@ class FeelPopup {
5906
6089
  }
5907
6090
 
5908
6091
  /**
5909
- * Open the FEEL popup.
6092
+ * Open the popup.
5910
6093
  *
5911
6094
  * @param {String} entryId
5912
6095
  * @param {Object} popupConfig
@@ -5915,7 +6098,7 @@ class FeelPopup {
5915
6098
  open(entryId, popupConfig, sourceElement) {
5916
6099
  // close before opening a new one
5917
6100
  this.close();
5918
- this._openPopup({
6101
+ return this._openPopup({
5919
6102
  ...popupConfig,
5920
6103
  entryId,
5921
6104
  sourceElement
@@ -5923,7 +6106,7 @@ class FeelPopup {
5923
6106
  }
5924
6107
 
5925
6108
  /**
5926
- * Close the FEEL popup.
6109
+ * Close the popup.
5927
6110
  */
5928
6111
  close() {
5929
6112
  this._closePopup();
@@ -5933,13 +6116,18 @@ class FeelPopup {
5933
6116
  element,
5934
6117
  label,
5935
6118
  sourceElement,
5936
- type
6119
+ type = DEFAULT_POPUP_TYPE
5937
6120
  } = context;
6121
+ const component = this._getProviders(type)[0];
6122
+ if (!component) {
6123
+ return false;
6124
+ }
5938
6125
  this._isOpen = true;
5939
6126
  this._eventBus.fire('propertiesPanelPopup.open', {
5940
6127
  container: this._config.feelPopupContainer,
5941
6128
  config: {
5942
6129
  ...context,
6130
+ component,
5943
6131
  links: this._config.getFeelPopupLinks?.(type) || [],
5944
6132
  onClose: () => {
5945
6133
  this._closePopup();
@@ -5949,13 +6137,13 @@ class FeelPopup {
5949
6137
  sourceElement && sourceElement.focus();
5950
6138
  }, 0);
5951
6139
  },
5952
- position: getPopupPosition(),
5953
6140
  title: getPopupTitle({
5954
6141
  element,
5955
6142
  label
5956
6143
  })
5957
6144
  }
5958
6145
  });
6146
+ return true;
5959
6147
  }
5960
6148
  _closePopup() {
5961
6149
  if (this._isOpen) {
@@ -5964,8 +6152,8 @@ class FeelPopup {
5964
6152
  }
5965
6153
  }
5966
6154
  }
5967
- FeelPopup.$inject = ['eventBus', 'config.propertiesPanel'];
5968
- class FeelPopupRenderer {
6155
+ Popup.$inject = ['eventBus', 'config.propertiesPanel'];
6156
+ class PopupRenderer {
5969
6157
  constructor(eventBus) {
5970
6158
  this._eventBus = eventBus;
5971
6159
  this._container = null;
@@ -5985,15 +6173,16 @@ class FeelPopupRenderer {
5985
6173
  container = this._container = getContainer(container) || document.body;
5986
6174
  const element = this._element = createElement();
5987
6175
  container.appendChild(element);
5988
-
5989
- // TODO(philippfromme): there is no useService in this context, so we need
5990
- // to pass the event bus as a prop or create a context provider, however,
5991
- // a custom renderer would have to use that context provider as well to have
5992
- // access to the event bus and other services
5993
6176
  this._emit('feelPopup.open');
5994
- preact.render(jsxRuntime.jsx(FeelPopup$1, {
5995
- ...config,
5996
- eventBus: this._eventBus
6177
+ const Component = config.component;
6178
+ preact.render(jsxRuntime.jsx(EventContext.Provider, {
6179
+ value: {
6180
+ eventBus: this._eventBus
6181
+ },
6182
+ children: jsxRuntime.jsx(Component, {
6183
+ ...config,
6184
+ eventBus: this._eventBus
6185
+ })
5997
6186
  }), element);
5998
6187
  this._emit('feelPopup.opened', {
5999
6188
  domNode: element
@@ -6009,13 +6198,14 @@ class FeelPopupRenderer {
6009
6198
  preact.render(null, this._element);
6010
6199
  this._container.removeChild(this._element);
6011
6200
  this._container = null;
6201
+ this._element = null;
6012
6202
  this._emit('feelPopup.closed');
6013
6203
  }
6014
6204
  _emit(event, context) {
6015
6205
  this._eventBus.fire(event, context);
6016
6206
  }
6017
6207
  }
6018
- FeelPopupRenderer.$inject = ['eventBus'];
6208
+ PopupRenderer.$inject = ['eventBus'];
6019
6209
 
6020
6210
  // helpers /////////////////
6021
6211
 
@@ -6032,8 +6222,8 @@ function getContainer(container) {
6032
6222
  }
6033
6223
  var index = {
6034
6224
  __init__: ['feelPopup', 'feelPopupRenderer'],
6035
- feelPopup: ['type', FeelPopup],
6036
- feelPopupRenderer: ['type', FeelPopupRenderer]
6225
+ feelPopup: ['type', Popup],
6226
+ feelPopupRenderer: ['type', PopupRenderer]
6037
6227
  };
6038
6228
 
6039
6229
  function getPropertiesPanelHeaderProvider(options = {}) {