@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.es.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { Ids } from 'ids';
2
- import { FormFieldRegistry as FormFieldRegistry$1, iconsByType, Label as Label$3, IFrame, Text as Text$1, Html, Table, ExpressionField, DocumentPreview, FormFields, sanitizeImageSource, getSchemaVariables, getAncestryList, FormContext, FormRenderContext, FormComponent, getScrollContainer, FieldFactory, FormLayouter, PathRegistry, Importer, FeelExpressionLanguage, OPTIONS_SOURCES, OPTIONS_SOURCES_PATHS, clone, runRecursively, 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';
2
+ import { FormFieldRegistry as FormFieldRegistry$1, iconsByType, Label as Label$3, IFrame, Text as Text$1, Html, Table, ExpressionField, DocumentPreview, FormFields, sanitizeImageSource, getSchemaVariables, getAncestryList, FormContext, FormRenderContext, FormComponent, getScrollContainer as getScrollContainer$1, FieldFactory, FormLayouter, PathRegistry, Importer, FeelExpressionLanguage, OPTIONS_SOURCES, OPTIONS_SOURCES_PATHS, clone, runRecursively, 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
- import { isArray, isFunction, isNumber, bind, assign, debounce, isString, set as set$1, get, reduce, forEach, uniqueBy, isObject, isDefined, isNil, without, has } from 'min-dash';
4
+ import { isArray, isFunction, isNumber, bind, assign, debounce, isString, isDefined, set as set$1, get, reduce, forEach, uniqueBy, isObject, isNil, without, has } from 'min-dash';
5
5
  import classnames from 'classnames';
6
6
  import { jsxs, jsx, Fragment as Fragment$1 } from 'preact/jsx-runtime';
7
7
  import { useContext, useRef, useEffect, useMemo, useState, useCallback, useLayoutEffect } from 'preact/hooks';
8
8
  import { createContext, Fragment, render, createElement as createElement$1 } from 'preact';
9
9
  import * as React from 'preact/compat';
10
10
  import { createPortal, useRef as useRef$1, useContext as useContext$1, useEffect as useEffect$1, forwardRef } from 'preact/compat';
11
- import { query, domify, classes, event } from 'min-dom';
11
+ import { query, closest, domify, classes, event } from 'min-dom';
12
12
  import { EditorView, lineNumbers } from '@codemirror/view';
13
13
  import { Annotation } from '@codemirror/state';
14
14
  import '@codemirror/language';
@@ -64,7 +64,7 @@ var TranslateModule = {
64
64
  };
65
65
 
66
66
  var FN_REF = '__fn';
67
- var DEFAULT_PRIORITY$4 = 1000;
67
+ var DEFAULT_PRIORITY$5 = 1000;
68
68
  var slice = Array.prototype.slice;
69
69
 
70
70
  /**
@@ -232,7 +232,7 @@ EventBus.prototype.on = function (events, priority, callback, that) {
232
232
  if (isFunction(priority)) {
233
233
  that = callback;
234
234
  callback = priority;
235
- priority = DEFAULT_PRIORITY$4;
235
+ priority = DEFAULT_PRIORITY$5;
236
236
  }
237
237
  if (!isNumber(priority)) {
238
238
  throw new Error('priority must be a number');
@@ -283,7 +283,7 @@ EventBus.prototype.once = function (events, priority, callback, that) {
283
283
  if (isFunction(priority)) {
284
284
  that = callback;
285
285
  callback = priority;
286
- priority = DEFAULT_PRIORITY$4;
286
+ priority = DEFAULT_PRIORITY$5;
287
287
  }
288
288
  if (!isNumber(priority)) {
289
289
  throw new Error('priority must be a number');
@@ -1365,7 +1365,7 @@ const ModularSection = props => {
1365
1365
  }) => RootElement ? jsx(RootElement, {
1366
1366
  children: children
1367
1367
  }) : jsx("div", {
1368
- className: rootClass,
1368
+ className: classnames(rootClass, 'bio-theme-parent'),
1369
1369
  children: children
1370
1370
  }), [rootClass, RootElement]);
1371
1371
  return shouldRender ? parent ? createPortal(jsx(Root, {
@@ -2076,7 +2076,8 @@ const EventContext = createContext({
2076
2076
  * @typedef { {
2077
2077
  * parserDialect?: import('@bpmn-io/feel-editor').ParserDialect,
2078
2078
  * builtins?: import('@bpmn-io/feel-editor').Variable[],
2079
- * dialect?: import('@bpmn-io/feel-editor').Dialect
2079
+ * dialect?: import('@bpmn-io/feel-editor').Dialect,
2080
+ * engines?: Record<string, string>
2080
2081
  * } } FeelLanguageContextType
2081
2082
  */
2082
2083
 
@@ -2278,7 +2279,7 @@ function Tooltip(props) {
2278
2279
  const tooltipStyle = position || (tooltipPosition ? `right: ${tooltipPosition.right}; top: ${tooltipPosition.top}px;` : undefined);
2279
2280
  const arrowStyle = arrowOffset != null ? `margin-top: ${arrowOffset}px;` : undefined;
2280
2281
  return jsxs("div", {
2281
- class: `bio-properties-panel-tooltip ${direction}`,
2282
+ class: `bio-properties-panel-tooltip bio-theme-parent ${direction}`,
2282
2283
  role: "tooltip",
2283
2284
  id: "bio-properties-panel-tooltip",
2284
2285
  "aria-labelledby": forId,
@@ -2516,24 +2517,32 @@ function useShowEntryEvent(id) {
2516
2517
  * @param {boolean} value
2517
2518
  */
2518
2519
 
2520
+ /**
2521
+ * Resolve scroll container closest to the referenced element.
2522
+ */
2523
+ function getScrollContainer(ref, scrollContainerSelector) {
2524
+ return ref.current ? closest(ref.current, scrollContainerSelector, true) : null;
2525
+ }
2526
+
2519
2527
  /**
2520
2528
  * Use IntersectionObserver to identify when DOM element is in sticky mode.
2529
+ *
2521
2530
  * If sticky is observered setSticky(true) will be called.
2522
2531
  * If sticky mode is left, setSticky(false) will be called.
2523
2532
  *
2524
- *
2525
2533
  * @param {Object} ref
2526
2534
  * @param {string} scrollContainerSelector
2527
2535
  * @param {setSticky} setSticky
2528
2536
  */
2529
2537
  function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky) {
2530
- const [scrollContainer, setScrollContainer] = useState(query(scrollContainerSelector));
2538
+ // resolved from the observed element after mount (see effect below)
2539
+ const [scrollContainer, setScrollContainer] = useState(null);
2531
2540
  const updateScrollContainer = useCallback(() => {
2532
- const newScrollContainer = query(scrollContainerSelector);
2541
+ const newScrollContainer = getScrollContainer(ref, scrollContainerSelector);
2533
2542
  if (newScrollContainer !== scrollContainer) {
2534
2543
  setScrollContainer(newScrollContainer);
2535
2544
  }
2536
- }, [scrollContainerSelector, scrollContainer]);
2545
+ }, [ref, scrollContainerSelector, scrollContainer]);
2537
2546
  useEffect(() => {
2538
2547
  updateScrollContainer();
2539
2548
  }, [updateScrollContainer]);
@@ -2546,9 +2555,8 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
2546
2555
  if (!Observer) {
2547
2556
  return;
2548
2557
  }
2549
-
2550
- // TODO(@barmac): test this
2551
- if (!ref.current || !scrollContainer) {
2558
+ const element = ref.current;
2559
+ if (!element || !scrollContainer) {
2552
2560
  return;
2553
2561
  }
2554
2562
  const observer = new Observer(entries => {
@@ -2569,13 +2577,13 @@ function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky)
2569
2577
  // Use bottom margin to avoid stickyness when scrolling out to bottom
2570
2578
  threshold: [1]
2571
2579
  });
2572
- observer.observe(ref.current);
2580
+ observer.observe(element);
2573
2581
 
2574
2582
  // Unobserve if unmounted
2575
2583
  return () => {
2576
- observer.unobserve(ref.current);
2584
+ observer.unobserve(element);
2577
2585
  };
2578
- }, [ref.current, scrollContainer, setSticky]);
2586
+ }, [ref, scrollContainer, setSticky]);
2579
2587
  }
2580
2588
 
2581
2589
  /**
@@ -2602,23 +2610,6 @@ function useStaticCallback(callback) {
2602
2610
  });
2603
2611
  return useCallback((...args) => callbackRef.current(...args), []);
2604
2612
  }
2605
- function useElementVisible(element) {
2606
- const [visible, setVisible] = useState(!!element && !!element.clientHeight);
2607
- useLayoutEffect(() => {
2608
- if (!element) return;
2609
- const resizeObserver = new ResizeObserver(([entry]) => {
2610
- requestAnimationFrame(() => {
2611
- const newVisible = !!entry.contentRect.height;
2612
- if (newVisible !== visible) {
2613
- setVisible(newVisible);
2614
- }
2615
- });
2616
- });
2617
- resizeObserver.observe(element);
2618
- return () => resizeObserver.disconnect();
2619
- }, [element, visible]);
2620
- return visible;
2621
- }
2622
2613
 
2623
2614
  /**
2624
2615
  * @param {import('../PropertiesPanel').GroupDefinition} props
@@ -2970,7 +2961,7 @@ function PropertiesPanel$1(props) {
2970
2961
  children: jsx(EventContext.Provider, {
2971
2962
  value: eventContext,
2972
2963
  children: jsxs("div", {
2973
- class: "bio-properties-panel",
2964
+ class: "bio-properties-panel bio-theme-parent",
2974
2965
  children: [headerProvider ? jsx(Header, {
2975
2966
  element: element,
2976
2967
  headerProvider: headerProvider
@@ -3061,16 +3052,17 @@ function CollapsibleEntry(props) {
3061
3052
  }, [onShow, setOpen])
3062
3053
  };
3063
3054
  const placeholderLabel = translate('<empty>');
3055
+ const hasEntries = entries.length > 0;
3064
3056
  return jsxs("div", {
3065
3057
  "data-entry-id": id,
3066
3058
  class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
3067
3059
  children: [jsxs("div", {
3068
3060
  class: "bio-properties-panel-collapsible-entry-header",
3069
- onClick: toggleOpen,
3061
+ onClick: hasEntries ? toggleOpen : null,
3070
3062
  children: [jsx("div", {
3071
3063
  class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
3072
3064
  children: label || placeholderLabel
3073
- }), jsx("button", {
3065
+ }), hasEntries && jsx("button", {
3074
3066
  type: "button",
3075
3067
  title: translate('Toggle list item'),
3076
3068
  class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
@@ -3586,7 +3578,8 @@ const FeelEditor = forwardRef((props, ref) => {
3586
3578
  const {
3587
3579
  builtins,
3588
3580
  dialect,
3589
- parserDialect
3581
+ parserDialect,
3582
+ engines
3590
3583
  } = feelLanguageContext || {};
3591
3584
  useBufferedFocus(editor, ref);
3592
3585
  const handleInput = useStaticCallback(newValue => {
@@ -3629,6 +3622,7 @@ const FeelEditor = forwardRef((props, ref) => {
3629
3622
  builtins,
3630
3623
  dialect,
3631
3624
  parserDialect,
3625
+ engines,
3632
3626
  extensions: [...(enableGutters ? [lineNumbers()] : []), EditorView.lineWrapping, EditorView.domEventHandlers({
3633
3627
  blur: handleBlur
3634
3628
  })],
@@ -3663,6 +3657,12 @@ const FeelEditor = forwardRef((props, ref) => {
3663
3657
  }
3664
3658
  editor.setPlaceholder(placeholder);
3665
3659
  }, [placeholder]);
3660
+ useEffect(() => {
3661
+ if (!editor) {
3662
+ return;
3663
+ }
3664
+ editor.setEngines(engines);
3665
+ }, [engines]);
3666
3666
  const handleClick = () => {
3667
3667
  ref.current.focus();
3668
3668
  };
@@ -4984,16 +4984,10 @@ function isEdited$3(node) {
4984
4984
  function prefixId$4(id) {
4985
4985
  return `bio-properties-panel-${id}`;
4986
4986
  }
4987
- function resizeToContents(element) {
4988
- element.style.height = 'auto';
4989
-
4990
- // a 2px pixel offset is required to prevent scrollbar from
4991
- // appearing on OS with a full length scroll bar (Windows/Linux)
4992
- element.style.height = `${element.scrollHeight + 2}px`;
4993
- }
4994
4987
  function TextArea(props) {
4995
4988
  const {
4996
4989
  id,
4990
+ element,
4997
4991
  label,
4998
4992
  debounce,
4999
4993
  onInput: commitValue,
@@ -5006,22 +5000,31 @@ function TextArea(props) {
5006
5000
  autoResize = true,
5007
5001
  placeholder,
5008
5002
  rows = autoResize ? 1 : 2,
5009
- tooltip
5003
+ tooltip,
5004
+ translate = translateFallback
5010
5005
  } = props;
5011
5006
  const [localValue, setLocalValue] = useState(value);
5012
5007
  const ref = useShowEntryEvent(id);
5008
+ const containerRef = useRef();
5009
+
5010
+ // keep a live reference to the current value so callbacks captured by the
5011
+ // popup (frozen at open time) can compare against the latest value
5012
+ const localValueRef = useRef(localValue);
5013
+ localValueRef.current = localValue;
5014
+ const {
5015
+ eventBus
5016
+ } = useContext(EventContext);
5017
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
5013
5018
  const onInput = useCallback(newValue => {
5014
5019
  const newModelValue = newValue === '' ? undefined : newValue;
5015
5020
  commitValue(newModelValue);
5016
5021
  }, [commitValue]);
5017
- const visible = useElementVisible(ref.current);
5018
5022
 
5019
5023
  /**
5020
5024
  * @type { import('min-dash').DebouncedFunction }
5021
5025
  */
5022
5026
  const handleInput = useDebounce(onInput, debounce);
5023
5027
  const handleLocalInput = e => {
5024
- autoResize && resizeToContents(e.target);
5025
5028
  if (e.target.value === localValue) {
5026
5029
  return;
5027
5030
  }
@@ -5066,12 +5069,35 @@ function TextArea(props) {
5066
5069
  handleInput.flush?.();
5067
5070
  }
5068
5071
  };
5069
- useLayoutEffect(() => {
5070
- autoResize && resizeToContents(ref.current);
5072
+ const handlePopupInput = newValue => {
5073
+ if (newValue === localValueRef.current) {
5074
+ return;
5075
+ }
5076
+ setLocalValue(newValue);
5077
+ handleInput(newValue);
5078
+ };
5079
+ const handleOpenPopup = () => {
5080
+ const isOpen = eventBus.fire('propertiesPanel.openPopup', {
5081
+ element,
5082
+ entryId: id,
5083
+ label,
5084
+ onInput: handlePopupInput,
5085
+ sourceElement: ref.current,
5086
+ value: localValue
5087
+ });
5088
+ if (isOpen) {
5089
+ eventBus.once('propertiesPanelPopup.close', () => {
5090
+ handleInput.flush?.();
5091
+ setIsPopupOpen(false);
5092
+ });
5093
+ }
5094
+ setIsPopupOpen(isOpen === true);
5095
+ };
5096
+ useEffect(() => {
5097
+ return () => {
5098
+ eventBus && eventBus.fire('propertiesPanel.closePopup');
5099
+ };
5071
5100
  }, []);
5072
- useLayoutEffect(() => {
5073
- visible && autoResize && resizeToContents(ref.current);
5074
- }, [visible]);
5075
5101
  useEffect(() => {
5076
5102
  if (value === localValue) {
5077
5103
  return;
@@ -5086,25 +5112,35 @@ function TextArea(props) {
5086
5112
  children: jsx(TooltipWrapper, {
5087
5113
  value: tooltip,
5088
5114
  forId: id,
5089
- element: props.element,
5115
+ element: element,
5090
5116
  children: label
5091
5117
  })
5092
- }), jsx("textarea", {
5093
- ref: ref,
5094
- id: prefixId$2(id),
5095
- name: id,
5096
- spellCheck: "false",
5097
- class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
5098
- onInput: handleLocalInput,
5099
- onFocus: onFocus,
5100
- onKeyDown: handleOnKeyDown,
5101
- onBlur: handleOnBlur,
5102
- onPaste: handleOnPaste,
5103
- placeholder: placeholder,
5104
- rows: rows,
5105
- value: localValue,
5106
- disabled: disabled,
5107
- "data-gramm": "false"
5118
+ }), jsxs("div", {
5119
+ class: classnames('bio-properties-panel-textarea-container', isPopupOpen ? 'popupOpen' : null),
5120
+ ref: containerRef,
5121
+ children: [isPopupOpen && jsx("div", {
5122
+ class: "bio-properties-panel-textarea__open-popup-placeholder",
5123
+ children: translate('Opened in editor')
5124
+ }), jsx("textarea", {
5125
+ ref: ref,
5126
+ id: prefixId$2(id),
5127
+ name: id,
5128
+ spellCheck: "false",
5129
+ class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
5130
+ onInput: handleLocalInput,
5131
+ onFocus: onFocus,
5132
+ onKeyDown: handleOnKeyDown,
5133
+ onBlur: handleOnBlur,
5134
+ onPaste: handleOnPaste,
5135
+ placeholder: placeholder,
5136
+ rows: rows,
5137
+ value: localValue,
5138
+ disabled: disabled,
5139
+ "data-gramm": "false"
5140
+ }), eventBus && !disabled && jsx(OpenPopupButton, {
5141
+ onClick: handleOpenPopup,
5142
+ translate: translate
5143
+ })]
5108
5144
  })]
5109
5145
  });
5110
5146
  }
@@ -5125,6 +5161,7 @@ function TextArea(props) {
5125
5161
  * @param {boolean} props.monospace
5126
5162
  * @param {Function} [props.validate]
5127
5163
  * @param {boolean} [props.disabled]
5164
+ * @param {Function} [props.translate]
5128
5165
  */
5129
5166
  function TextAreaEntry(props) {
5130
5167
  const {
@@ -5144,7 +5181,8 @@ function TextAreaEntry(props) {
5144
5181
  onPaste,
5145
5182
  placeholder,
5146
5183
  autoResize,
5147
- tooltip
5184
+ tooltip,
5185
+ translate
5148
5186
  } = props;
5149
5187
  const globalError = useError(id);
5150
5188
  const [localError, setLocalError] = useState(null);
@@ -5185,6 +5223,7 @@ function TextAreaEntry(props) {
5185
5223
  placeholder: placeholder,
5186
5224
  autoResize: autoResize,
5187
5225
  tooltip: tooltip,
5226
+ translate: translate,
5188
5227
  element: element
5189
5228
  }, element), error && jsx("div", {
5190
5229
  class: "bio-properties-panel-error",
@@ -5400,6 +5439,16 @@ function isEdited(node) {
5400
5439
  function prefixId$1(id) {
5401
5440
  return `bio-properties-panel-${id}`;
5402
5441
  }
5442
+ function getPopupTitle({
5443
+ element,
5444
+ label
5445
+ }) {
5446
+ let popupTitle = '';
5447
+ if (element && element.type) {
5448
+ popupTitle = `${element.type} / `;
5449
+ }
5450
+ return `${popupTitle}${label}`;
5451
+ }
5403
5452
 
5404
5453
  /**
5405
5454
  * Add a dragger that calls back the passed function with
@@ -5484,6 +5533,9 @@ const noop = () => {};
5484
5533
  * @param {Function} [props.onPostDeactivate]
5485
5534
  * @param {boolean} [props.returnFocus]
5486
5535
  * @param {boolean} [props.closeOnEscape]
5536
+ * @param {(event: KeyboardEvent) => boolean} [props.allowFocusMove] -
5537
+ * Whether a Tab keypress may move focus. Return false to leave the keypress
5538
+ * to the focused element, e.g. an editor navigating snippet placeholders.
5487
5539
  * @param {string} props.title
5488
5540
  * @param {Ref} [ref]
5489
5541
  */
@@ -5499,6 +5551,7 @@ function PopupComponent(props, globalRef) {
5499
5551
  onPostDeactivate = noop,
5500
5552
  returnFocus = true,
5501
5553
  closeOnEscape = true,
5554
+ allowFocusMove = () => true,
5502
5555
  title
5503
5556
  } = props;
5504
5557
  const focusTrapRef = useRef(null);
@@ -5518,13 +5571,14 @@ function PopupComponent(props, globalRef) {
5518
5571
  focusTrapRef.current.activate();
5519
5572
  }
5520
5573
  };
5521
- let style = {};
5522
- if (position) {
5523
- style = {
5524
- ...style,
5525
- top: position.top + 'px',
5526
- left: position.left + 'px'
5527
- };
5574
+ const {
5575
+ top,
5576
+ left
5577
+ } = position || getCenterPosition(width, height);
5578
+ const style = {};
5579
+ if (isDefined(top) && isDefined(left)) {
5580
+ style.top = top + 'px';
5581
+ style.left = left + 'px';
5528
5582
  }
5529
5583
  if (width) {
5530
5584
  style.width = width + 'px';
@@ -5548,6 +5602,8 @@ function PopupComponent(props, globalRef) {
5548
5602
  clickOutsideDeactivates: true,
5549
5603
  delayInitialFocus,
5550
5604
  fallbackFocus: popupRef.current,
5605
+ isKeyForward: event => isTab(event) && !event.shiftKey && allowFocusMove(event),
5606
+ isKeyBackward: event => isTab(event) && event.shiftKey && allowFocusMove(event),
5551
5607
  onPostActivate,
5552
5608
  onPostDeactivate,
5553
5609
  returnFocusOnDeactivate: returnFocus
@@ -5562,21 +5618,23 @@ function PopupComponent(props, globalRef) {
5562
5618
  ref: popupRef,
5563
5619
  onKeyDown: handleKeydown,
5564
5620
  role: "dialog",
5565
- class: classnames('bio-properties-panel-popup', className),
5621
+ class: classnames('bio-properties-panel-popup', 'bio-theme-parent', className),
5566
5622
  style: style,
5567
5623
  children: props.children
5568
5624
  });
5569
5625
  }
5570
- const Popup = forwardRef(PopupComponent);
5571
- Popup.Title = Title;
5572
- Popup.Body = Body;
5573
- Popup.Footer = Footer;
5626
+ const Popup$1 = forwardRef(PopupComponent);
5627
+ Popup$1.Title = Title;
5628
+ Popup$1.Body = Body;
5629
+ Popup$1.Footer = Footer;
5574
5630
  function Title(props) {
5631
+ const {
5632
+ eventBus
5633
+ } = useContext(EventContext);
5575
5634
  const {
5576
5635
  children,
5577
5636
  className,
5578
5637
  draggable,
5579
- eventBus,
5580
5638
  title,
5581
5639
  showCloseButton = false,
5582
5640
  closeButtonTooltip = 'Close popup',
@@ -5681,6 +5739,26 @@ function Footer(props) {
5681
5739
 
5682
5740
  // helpers //////////////////////
5683
5741
 
5742
+ /**
5743
+ * Center the popup in the viewport, based on its own dimensions.
5744
+ *
5745
+ * @param {number} [width]
5746
+ * @param {number} [height]
5747
+ */
5748
+ function getCenterPosition(width, height) {
5749
+ if (!width || !height) {
5750
+ return {};
5751
+ }
5752
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
5753
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
5754
+ return {
5755
+ left: Math.max(0, (viewportWidth - width) / 2),
5756
+ top: Math.max(0, (viewportHeight - height) / 2)
5757
+ };
5758
+ }
5759
+ function isTab(event) {
5760
+ return event.key === 'Tab';
5761
+ }
5684
5762
  function getPopupParent(node) {
5685
5763
  return node.closest('.bio-properties-panel-popup');
5686
5764
  }
@@ -5699,22 +5777,20 @@ function cancel(event) {
5699
5777
  * @property {string} value
5700
5778
  * @property {Array} [links]
5701
5779
  * @property {Array|Object} [variables]
5702
- * @property {Object} [position]
5703
5780
  * @property {string} [hostLanguage]
5704
5781
  * @property {boolean} [singleLine]
5705
5782
  * @property {HTMLElement} [sourceElement]
5706
5783
  * @property {HTMLElement|string} [tooltipContainer]
5707
- * @property {Object} [eventBus]
5708
5784
  */
5709
5785
 
5710
5786
  const FEEL_POPUP_WIDTH = 700;
5711
- const FEEL_POPUP_HEIGHT = 250;
5787
+ const FEEL_POPUP_HEIGHT = 400;
5712
5788
 
5713
5789
  /**
5714
5790
  * FEEL expression editor popup component
5715
5791
  * @param {FeelPopupProps} props
5716
5792
  */
5717
- function FeelPopup$1(props) {
5793
+ function FeelPopup(props) {
5718
5794
  const {
5719
5795
  entryId,
5720
5796
  onInput,
@@ -5724,12 +5800,10 @@ function FeelPopup$1(props) {
5724
5800
  value,
5725
5801
  links,
5726
5802
  variables,
5727
- position,
5728
5803
  hostLanguage,
5729
5804
  singleLine,
5730
5805
  sourceElement,
5731
5806
  tooltipContainer,
5732
- eventBus,
5733
5807
  feelLanguageContext
5734
5808
  } = props;
5735
5809
  const editorRef = useRef();
@@ -5759,20 +5833,19 @@ function FeelPopup$1(props) {
5759
5833
  editorRef.current.focus();
5760
5834
  }
5761
5835
  }, [editorRef]);
5762
- return jsxs(Popup, {
5836
+ return jsxs(Popup$1, {
5763
5837
  className: "bio-properties-panel-feel-popup",
5764
- position: position,
5765
5838
  title: title,
5766
5839
  returnFocus: false,
5767
5840
  closeOnEscape: false,
5768
5841
  delayInitialFocus: false,
5842
+ allowFocusMove: event => !isSnippetNavigation(event),
5769
5843
  onPostDeactivate: handleSetReturnFocus,
5770
5844
  height: FEEL_POPUP_HEIGHT,
5771
5845
  width: FEEL_POPUP_WIDTH,
5772
5846
  ref: popupRef,
5773
- children: [jsx(Popup.Title, {
5847
+ children: [jsx(Popup$1.Title, {
5774
5848
  title: title,
5775
- eventBus: eventBus,
5776
5849
  showCloseButton: true,
5777
5850
  closeButtonTooltip: "Save and close",
5778
5851
  onClose: onClose,
@@ -5788,14 +5861,13 @@ function FeelPopup$1(props) {
5788
5861
  }, index);
5789
5862
  })
5790
5863
  })
5791
- }), jsx(Popup.Body, {
5864
+ }), jsx(Popup$1.Body, {
5792
5865
  children: jsxs("div", {
5793
5866
  onKeyDownCapture: onKeyDownCapture,
5794
5867
  onKeyDown: onKeyDown,
5795
5868
  class: "bio-properties-panel-feel-popup__body",
5796
5869
  children: [type === 'feel' && jsx(FeelEditor, {
5797
5870
  enableGutters: true,
5798
- id: prefixId(entryId),
5799
5871
  name: entryId,
5800
5872
  onInput: onInput,
5801
5873
  value: value,
@@ -5804,7 +5876,6 @@ function FeelPopup$1(props) {
5804
5876
  ref: editorRef,
5805
5877
  tooltipContainer: tooltipContainer
5806
5878
  }), type === 'feelers' && jsx(TemplatingEditor, {
5807
- id: prefixId(entryId),
5808
5879
  contentAttributes: {
5809
5880
  'aria-label': title
5810
5881
  },
@@ -5821,34 +5892,107 @@ function FeelPopup$1(props) {
5821
5892
  })]
5822
5893
  });
5823
5894
  }
5824
- function prefixId(id) {
5825
- return `bio-properties-panel-${id}`;
5826
- }
5827
5895
  function autoCompletionOpen(element) {
5828
5896
  const editor = element.closest('.cm-editor');
5829
5897
  return editor ? editor.querySelector('.cm-tooltip-autocomplete') : null;
5830
5898
  }
5831
- function getPopupTitle({
5832
- element,
5833
- label
5834
- }) {
5835
- let popupTitle = '';
5836
- if (element && element.type) {
5837
- popupTitle = `${element.type} / `;
5838
- }
5839
- return `${popupTitle}${label}`;
5899
+
5900
+ // while a snippet is active, Tab navigates its placeholders inside the editor
5901
+ function isSnippetNavigation(event) {
5902
+ const editor = event.target.closest('.cm-editor');
5903
+ return !!(editor && editor.querySelector('.cm-snippetField'));
5840
5904
  }
5841
- function getPopupPosition() {
5842
- const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
5843
- const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
5844
- return {
5845
- left: Math.max(0, (viewportWidth - FEEL_POPUP_WIDTH) / 2),
5846
- top: Math.max(0, (viewportHeight - FEEL_POPUP_HEIGHT) / 2)
5905
+
5906
+ /**
5907
+ * @typedef {Object} TextPopupProps
5908
+ * @property {string} entryId
5909
+ * @property {Function} onInput
5910
+ * @property {Function} onClose
5911
+ * @property {string} title
5912
+ * @property {string} value
5913
+ * @property {HTMLElement} [sourceElement]
5914
+ */
5915
+
5916
+ const TEXT_POPUP_WIDTH = 700;
5917
+ const TEXT_POPUP_HEIGHT = 400;
5918
+
5919
+ /**
5920
+ * Plain text editor popup component.
5921
+ *
5922
+ * Intentionally kept separate from the FEEL popup: the two are expected to
5923
+ * diverge as we add FEEL-specific capabilities that do not apply to plain text.
5924
+ *
5925
+ * @param {TextPopupProps} props
5926
+ */
5927
+ function TextPopup(props) {
5928
+ const {
5929
+ entryId,
5930
+ onInput,
5931
+ onClose,
5932
+ title,
5933
+ value,
5934
+ sourceElement
5935
+ } = props;
5936
+ const editorRef = useRef();
5937
+ const popupRef = useRef();
5938
+ const handleSetReturnFocus = () => {
5939
+ sourceElement && sourceElement.focus();
5847
5940
  };
5941
+ useEffect(() => {
5942
+ // set focus on editor when popup is opened
5943
+ if (editorRef.current) {
5944
+ editorRef.current.focus();
5945
+ }
5946
+ }, [editorRef]);
5947
+ return jsxs(Popup$1, {
5948
+ className: "bio-properties-panel-text-popup",
5949
+ title: title,
5950
+ returnFocus: false,
5951
+ closeOnEscape: true,
5952
+ delayInitialFocus: false,
5953
+ onClose: onClose,
5954
+ onPostDeactivate: handleSetReturnFocus,
5955
+ height: TEXT_POPUP_HEIGHT,
5956
+ width: TEXT_POPUP_WIDTH,
5957
+ ref: popupRef,
5958
+ children: [jsx(Popup$1.Title, {
5959
+ title: title,
5960
+ showCloseButton: true,
5961
+ closeButtonTooltip: "Save and close",
5962
+ onClose: onClose,
5963
+ draggable: true
5964
+ }), jsx(Popup$1.Body, {
5965
+ children: jsx("textarea", {
5966
+ id: prefixId(entryId),
5967
+ name: entryId,
5968
+ class: "bio-properties-panel-input",
5969
+ ref: editorRef,
5970
+ onInput: e => onInput(e.target.value),
5971
+ value: value || '',
5972
+ spellCheck: "false",
5973
+ autoComplete: "off",
5974
+ "aria-label": title,
5975
+ "data-gramm": "false"
5976
+ })
5977
+ })]
5978
+ });
5979
+ }
5980
+
5981
+ // The in-panel field stays mounted (hidden) while the popup is open, so we
5982
+ // scope the popup editor id to keep it unique in the DOM.
5983
+ function prefixId(id) {
5984
+ return `bio-properties-panel-popup-${id}`;
5848
5985
  }
5986
+ const DEFAULT_POPUP_TYPE = 'text';
5987
+
5988
+ // consumers registering via #registerProvider default to DEFAULT_PRIORITY,
5989
+ // so their providers take precedence over the LOW_PRIORITY built-ins.
5990
+ const DEFAULT_PRIORITY$4 = 1000;
5991
+ const LOW_PRIORITY$2 = 500;
5849
5992
 
5850
5993
  /**
5851
- * FEEL popup component, built as a singleton.
5994
+ * Popup manager, built as a singleton. Renders the registered provider for a
5995
+ * given popup type; consumers may plug in their own via #registerProvider.
5852
5996
  *
5853
5997
  * In order to implement a custom replacement, handle the following events:
5854
5998
  * - `propertiesPanel.openPopup`
@@ -5861,16 +6005,19 @@ function getPopupPosition() {
5861
6005
  * - `feelPopup.close` - fired before the popup is unmounted. Event context contains the DOM node of the popup as `domNode`
5862
6006
  * - `feelPopup.closed` - fired after the popup is unmounted
5863
6007
  */
5864
- class FeelPopup {
6008
+ class Popup {
5865
6009
  constructor(eventBus, config = {}) {
5866
6010
  this._eventBus = eventBus;
5867
6011
  this._config = config;
5868
6012
  this._isOpen = false;
5869
- eventBus.on('propertiesPanel.openPopup', (_, context) => {
5870
- this.open(context.entryId, context, context.sourceElement);
5871
6013
 
5872
- // return true to indicate that popup was opened
5873
- return true;
6014
+ // built-in providers, registered at LOW_PRIORITY so consumers can
6015
+ // override them via #registerProvider using the default priority
6016
+ this.registerProvider('feel', LOW_PRIORITY$2, FeelPopup);
6017
+ this.registerProvider('feelers', LOW_PRIORITY$2, FeelPopup);
6018
+ this.registerProvider('text', LOW_PRIORITY$2, TextPopup);
6019
+ eventBus.on('propertiesPanel.openPopup', (_, context) => {
6020
+ return this.open(context.entryId, context, context.sourceElement);
5874
6021
  });
5875
6022
  eventBus.on(['propertiesPanel.closePopup', 'propertiesPanel.detach'], () => {
5876
6023
  this.close();
@@ -5878,7 +6025,43 @@ class FeelPopup {
5878
6025
  }
5879
6026
 
5880
6027
  /**
5881
- * Check if the FEEL popup is open.
6028
+ * Register a popup provider (component) for a given type.
6029
+ *
6030
+ * A higher `priority` wins when multiple providers are registered for the
6031
+ * same type; the built-in providers use a low priority so consumers override
6032
+ * them by default.
6033
+ *
6034
+ * @param {string} type
6035
+ * @param {number} [priority=DEFAULT_PRIORITY]
6036
+ * @param {Function|import('preact').Component} provider
6037
+ */
6038
+ registerProvider(type, priority, provider) {
6039
+ if (!provider) {
6040
+ provider = priority;
6041
+ priority = DEFAULT_PRIORITY$4;
6042
+ }
6043
+ this._eventBus.on('propertiesPanelPopup.getProviders.' + type, priority, function (event) {
6044
+ event.providers.push(provider);
6045
+ });
6046
+ }
6047
+
6048
+ /**
6049
+ * Get the popup providers registered for a type.
6050
+ *
6051
+ * @param {string} type
6052
+ * @return {Array<Function|import('preact').Component>}
6053
+ */
6054
+ _getProviders(type) {
6055
+ const event = this._eventBus.createEvent({
6056
+ type: 'propertiesPanelPopup.getProviders.' + type,
6057
+ providers: []
6058
+ });
6059
+ this._eventBus.fire(event);
6060
+ return event.providers;
6061
+ }
6062
+
6063
+ /**
6064
+ * Check if the popup is open.
5882
6065
  * @return {Boolean}
5883
6066
  */
5884
6067
  isOpen() {
@@ -5886,7 +6069,7 @@ class FeelPopup {
5886
6069
  }
5887
6070
 
5888
6071
  /**
5889
- * Open the FEEL popup.
6072
+ * Open the popup.
5890
6073
  *
5891
6074
  * @param {String} entryId
5892
6075
  * @param {Object} popupConfig
@@ -5895,7 +6078,7 @@ class FeelPopup {
5895
6078
  open(entryId, popupConfig, sourceElement) {
5896
6079
  // close before opening a new one
5897
6080
  this.close();
5898
- this._openPopup({
6081
+ return this._openPopup({
5899
6082
  ...popupConfig,
5900
6083
  entryId,
5901
6084
  sourceElement
@@ -5903,7 +6086,7 @@ class FeelPopup {
5903
6086
  }
5904
6087
 
5905
6088
  /**
5906
- * Close the FEEL popup.
6089
+ * Close the popup.
5907
6090
  */
5908
6091
  close() {
5909
6092
  this._closePopup();
@@ -5913,13 +6096,18 @@ class FeelPopup {
5913
6096
  element,
5914
6097
  label,
5915
6098
  sourceElement,
5916
- type
6099
+ type = DEFAULT_POPUP_TYPE
5917
6100
  } = context;
6101
+ const component = this._getProviders(type)[0];
6102
+ if (!component) {
6103
+ return false;
6104
+ }
5918
6105
  this._isOpen = true;
5919
6106
  this._eventBus.fire('propertiesPanelPopup.open', {
5920
6107
  container: this._config.feelPopupContainer,
5921
6108
  config: {
5922
6109
  ...context,
6110
+ component,
5923
6111
  links: this._config.getFeelPopupLinks?.(type) || [],
5924
6112
  onClose: () => {
5925
6113
  this._closePopup();
@@ -5929,13 +6117,13 @@ class FeelPopup {
5929
6117
  sourceElement && sourceElement.focus();
5930
6118
  }, 0);
5931
6119
  },
5932
- position: getPopupPosition(),
5933
6120
  title: getPopupTitle({
5934
6121
  element,
5935
6122
  label
5936
6123
  })
5937
6124
  }
5938
6125
  });
6126
+ return true;
5939
6127
  }
5940
6128
  _closePopup() {
5941
6129
  if (this._isOpen) {
@@ -5944,8 +6132,8 @@ class FeelPopup {
5944
6132
  }
5945
6133
  }
5946
6134
  }
5947
- FeelPopup.$inject = ['eventBus', 'config.propertiesPanel'];
5948
- class FeelPopupRenderer {
6135
+ Popup.$inject = ['eventBus', 'config.propertiesPanel'];
6136
+ class PopupRenderer {
5949
6137
  constructor(eventBus) {
5950
6138
  this._eventBus = eventBus;
5951
6139
  this._container = null;
@@ -5965,15 +6153,16 @@ class FeelPopupRenderer {
5965
6153
  container = this._container = getContainer(container) || document.body;
5966
6154
  const element = this._element = createElement();
5967
6155
  container.appendChild(element);
5968
-
5969
- // TODO(philippfromme): there is no useService in this context, so we need
5970
- // to pass the event bus as a prop or create a context provider, however,
5971
- // a custom renderer would have to use that context provider as well to have
5972
- // access to the event bus and other services
5973
6156
  this._emit('feelPopup.open');
5974
- render(jsx(FeelPopup$1, {
5975
- ...config,
5976
- eventBus: this._eventBus
6157
+ const Component = config.component;
6158
+ render(jsx(EventContext.Provider, {
6159
+ value: {
6160
+ eventBus: this._eventBus
6161
+ },
6162
+ children: jsx(Component, {
6163
+ ...config,
6164
+ eventBus: this._eventBus
6165
+ })
5977
6166
  }), element);
5978
6167
  this._emit('feelPopup.opened', {
5979
6168
  domNode: element
@@ -5989,13 +6178,14 @@ class FeelPopupRenderer {
5989
6178
  render(null, this._element);
5990
6179
  this._container.removeChild(this._element);
5991
6180
  this._container = null;
6181
+ this._element = null;
5992
6182
  this._emit('feelPopup.closed');
5993
6183
  }
5994
6184
  _emit(event, context) {
5995
6185
  this._eventBus.fire(event, context);
5996
6186
  }
5997
6187
  }
5998
- FeelPopupRenderer.$inject = ['eventBus'];
6188
+ PopupRenderer.$inject = ['eventBus'];
5999
6189
 
6000
6190
  // helpers /////////////////
6001
6191
 
@@ -6012,8 +6202,8 @@ function getContainer(container) {
6012
6202
  }
6013
6203
  var index = {
6014
6204
  __init__: ['feelPopup', 'feelPopupRenderer'],
6015
- feelPopup: ['type', FeelPopup],
6016
- feelPopupRenderer: ['type', FeelPopupRenderer]
6205
+ feelPopup: ['type', Popup],
6206
+ feelPopupRenderer: ['type', PopupRenderer]
6017
6207
  };
6018
6208
 
6019
6209
  function getPropertiesPanelHeaderProvider(options = {}) {
@@ -6775,7 +6965,7 @@ function Element$1(props) {
6775
6965
  function scrollIntoView({
6776
6966
  selection
6777
6967
  }) {
6778
- const scrollContainer = getScrollContainer(ref.current);
6968
+ const scrollContainer = getScrollContainer$1(ref.current);
6779
6969
  if (!selection || selection.type === 'default' || selection.id !== id || !scrollContainer || !ref.current) {
6780
6970
  return;
6781
6971
  }