@bpmn-io/properties-panel 3.47.0 → 3.48.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
@@ -4104,6 +4104,7 @@ function resizeToContents(element) {
4104
4104
  function TextArea(props) {
4105
4105
  const {
4106
4106
  id,
4107
+ element,
4107
4108
  label,
4108
4109
  debounce,
4109
4110
  onInput: commitValue,
@@ -4116,10 +4117,21 @@ function TextArea(props) {
4116
4117
  autoResize = true,
4117
4118
  placeholder,
4118
4119
  rows = autoResize ? 1 : 2,
4119
- tooltip
4120
+ tooltip,
4121
+ translate = translateFallback
4120
4122
  } = props;
4121
4123
  const [localValue, setLocalValue] = hooks.useState(value);
4122
4124
  const ref = useShowEntryEvent(id);
4125
+ const containerRef = hooks.useRef();
4126
+
4127
+ // keep a live reference to the current value so callbacks captured by the
4128
+ // popup (frozen at open time) can compare against the latest value
4129
+ const localValueRef = hooks.useRef(localValue);
4130
+ localValueRef.current = localValue;
4131
+ const {
4132
+ eventBus
4133
+ } = hooks.useContext(EventContext);
4134
+ const [isPopupOpen, setIsPopupOpen] = hooks.useState(false);
4123
4135
  const onInput = hooks.useCallback(newValue => {
4124
4136
  const newModelValue = newValue === '' ? undefined : newValue;
4125
4137
  commitValue(newModelValue);
@@ -4131,7 +4143,6 @@ function TextArea(props) {
4131
4143
  */
4132
4144
  const handleInput = useDebounce(onInput, debounce);
4133
4145
  const handleLocalInput = e => {
4134
- autoResize && resizeToContents(e.target);
4135
4146
  if (e.target.value === localValue) {
4136
4147
  return;
4137
4148
  }
@@ -4176,9 +4187,40 @@ function TextArea(props) {
4176
4187
  handleInput.flush?.();
4177
4188
  }
4178
4189
  };
4190
+ const handlePopupInput = newValue => {
4191
+ if (newValue === localValueRef.current) {
4192
+ return;
4193
+ }
4194
+ setLocalValue(newValue);
4195
+ handleInput(newValue);
4196
+ };
4197
+ const handleOpenPopup = () => {
4198
+ const isOpen = eventBus.fire('propertiesPanel.openPopup', {
4199
+ element,
4200
+ entryId: id,
4201
+ label,
4202
+ onInput: handlePopupInput,
4203
+ sourceElement: ref.current,
4204
+ value: localValue
4205
+ });
4206
+ if (isOpen) {
4207
+ eventBus.once('propertiesPanelPopup.close', () => {
4208
+ handleInput.flush?.();
4209
+ setIsPopupOpen(false);
4210
+ });
4211
+ }
4212
+ setIsPopupOpen(isOpen === true);
4213
+ };
4214
+ hooks.useEffect(() => {
4215
+ return () => {
4216
+ eventBus && eventBus.fire('propertiesPanel.closePopup');
4217
+ };
4218
+ }, []);
4219
+
4220
+ // auto-resize on any value change (typing, popup edits, external updates)
4179
4221
  hooks.useLayoutEffect(() => {
4180
4222
  autoResize && resizeToContents(ref.current);
4181
- }, []);
4223
+ }, [localValue]);
4182
4224
  hooks.useLayoutEffect(() => {
4183
4225
  visible && autoResize && resizeToContents(ref.current);
4184
4226
  }, [visible]);
@@ -4196,25 +4238,35 @@ function TextArea(props) {
4196
4238
  children: jsxRuntime.jsx(TooltipWrapper, {
4197
4239
  value: tooltip,
4198
4240
  forId: id,
4199
- element: props.element,
4241
+ element: element,
4200
4242
  children: label
4201
4243
  })
4202
- }), jsxRuntime.jsx("textarea", {
4203
- ref: ref,
4204
- id: prefixId$2(id),
4205
- name: id,
4206
- spellCheck: "false",
4207
- class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
4208
- onInput: handleLocalInput,
4209
- onFocus: onFocus,
4210
- onKeyDown: handleOnKeyDown,
4211
- onBlur: handleOnBlur,
4212
- onPaste: handleOnPaste,
4213
- placeholder: placeholder,
4214
- rows: rows,
4215
- value: localValue,
4216
- disabled: disabled,
4217
- "data-gramm": "false"
4244
+ }), jsxRuntime.jsxs("div", {
4245
+ class: classnames('bio-properties-panel-textarea-container', isPopupOpen ? 'popupOpen' : null),
4246
+ ref: containerRef,
4247
+ children: [isPopupOpen && jsxRuntime.jsx("div", {
4248
+ class: "bio-properties-panel-textarea__open-popup-placeholder",
4249
+ children: translate('Opened in editor')
4250
+ }), jsxRuntime.jsx("textarea", {
4251
+ ref: ref,
4252
+ id: prefixId$2(id),
4253
+ name: id,
4254
+ spellCheck: "false",
4255
+ class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
4256
+ onInput: handleLocalInput,
4257
+ onFocus: onFocus,
4258
+ onKeyDown: handleOnKeyDown,
4259
+ onBlur: handleOnBlur,
4260
+ onPaste: handleOnPaste,
4261
+ placeholder: placeholder,
4262
+ rows: rows,
4263
+ value: localValue,
4264
+ disabled: disabled,
4265
+ "data-gramm": "false"
4266
+ }), eventBus && !disabled && jsxRuntime.jsx(OpenPopupButton, {
4267
+ onClick: handleOpenPopup,
4268
+ translate: translate
4269
+ })]
4218
4270
  })]
4219
4271
  });
4220
4272
  }
@@ -4235,6 +4287,7 @@ function TextArea(props) {
4235
4287
  * @param {boolean} props.monospace
4236
4288
  * @param {Function} [props.validate]
4237
4289
  * @param {boolean} [props.disabled]
4290
+ * @param {Function} [props.translate]
4238
4291
  */
4239
4292
  function TextAreaEntry(props) {
4240
4293
  const {
@@ -4254,7 +4307,8 @@ function TextAreaEntry(props) {
4254
4307
  onPaste,
4255
4308
  placeholder,
4256
4309
  autoResize,
4257
- tooltip
4310
+ tooltip,
4311
+ translate
4258
4312
  } = props;
4259
4313
  const globalError = useError(id);
4260
4314
  const [localError, setLocalError] = hooks.useState(null);
@@ -4295,6 +4349,7 @@ function TextAreaEntry(props) {
4295
4349
  placeholder: placeholder,
4296
4350
  autoResize: autoResize,
4297
4351
  tooltip: tooltip,
4352
+ translate: translate,
4298
4353
  element: element
4299
4354
  }, element), error && jsxRuntime.jsx("div", {
4300
4355
  class: "bio-properties-panel-error",
@@ -4636,6 +4691,9 @@ const noop = () => {};
4636
4691
  * @param {Function} [props.onPostDeactivate]
4637
4692
  * @param {boolean} [props.returnFocus]
4638
4693
  * @param {boolean} [props.closeOnEscape]
4694
+ * @param {(event: KeyboardEvent) => boolean} [props.allowFocusMove] -
4695
+ * Whether a Tab keypress may move focus. Return false to leave the keypress
4696
+ * to the focused element, e.g. an editor navigating snippet placeholders.
4639
4697
  * @param {string} props.title
4640
4698
  * @param {Ref} [ref]
4641
4699
  */
@@ -4651,6 +4709,7 @@ function PopupComponent(props, globalRef) {
4651
4709
  onPostDeactivate = noop,
4652
4710
  returnFocus = true,
4653
4711
  closeOnEscape = true,
4712
+ allowFocusMove = () => true,
4654
4713
  title
4655
4714
  } = props;
4656
4715
  const focusTrapRef = hooks.useRef(null);
@@ -4700,6 +4759,8 @@ function PopupComponent(props, globalRef) {
4700
4759
  clickOutsideDeactivates: true,
4701
4760
  delayInitialFocus,
4702
4761
  fallbackFocus: popupRef.current,
4762
+ isKeyForward: event => isTab(event) && !event.shiftKey && allowFocusMove(event),
4763
+ isKeyBackward: event => isTab(event) && event.shiftKey && allowFocusMove(event),
4703
4764
  onPostActivate,
4704
4765
  onPostDeactivate,
4705
4766
  returnFocusOnDeactivate: returnFocus
@@ -4719,10 +4780,10 @@ function PopupComponent(props, globalRef) {
4719
4780
  children: props.children
4720
4781
  });
4721
4782
  }
4722
- const Popup = compat.forwardRef(PopupComponent);
4723
- Popup.Title = Title;
4724
- Popup.Body = Body;
4725
- Popup.Footer = Footer;
4783
+ const Popup$1 = compat.forwardRef(PopupComponent);
4784
+ Popup$1.Title = Title;
4785
+ Popup$1.Body = Body;
4786
+ Popup$1.Footer = Footer;
4726
4787
  function Title(props) {
4727
4788
  const {
4728
4789
  children,
@@ -4833,6 +4894,9 @@ function Footer(props) {
4833
4894
 
4834
4895
  // helpers //////////////////////
4835
4896
 
4897
+ function isTab(event) {
4898
+ return event.key === 'Tab';
4899
+ }
4836
4900
  function getPopupParent(node) {
4837
4901
  return node.closest('.bio-properties-panel-popup');
4838
4902
  }
@@ -4860,13 +4924,13 @@ function cancel(event) {
4860
4924
  */
4861
4925
 
4862
4926
  const FEEL_POPUP_WIDTH = 700;
4863
- const FEEL_POPUP_HEIGHT = 250;
4927
+ const FEEL_POPUP_HEIGHT = 400;
4864
4928
 
4865
4929
  /**
4866
4930
  * FEEL expression editor popup component
4867
4931
  * @param {FeelPopupProps} props
4868
4932
  */
4869
- function FeelPopup$1(props) {
4933
+ function FeelPopup(props) {
4870
4934
  const {
4871
4935
  entryId,
4872
4936
  onInput,
@@ -4911,18 +4975,19 @@ function FeelPopup$1(props) {
4911
4975
  editorRef.current.focus();
4912
4976
  }
4913
4977
  }, [editorRef]);
4914
- return jsxRuntime.jsxs(Popup, {
4978
+ return jsxRuntime.jsxs(Popup$1, {
4915
4979
  className: "bio-properties-panel-feel-popup",
4916
4980
  position: position,
4917
4981
  title: title,
4918
4982
  returnFocus: false,
4919
4983
  closeOnEscape: false,
4920
4984
  delayInitialFocus: false,
4985
+ allowFocusMove: event => !isSnippetNavigation(event),
4921
4986
  onPostDeactivate: handleSetReturnFocus,
4922
4987
  height: FEEL_POPUP_HEIGHT,
4923
4988
  width: FEEL_POPUP_WIDTH,
4924
4989
  ref: popupRef,
4925
- children: [jsxRuntime.jsx(Popup.Title, {
4990
+ children: [jsxRuntime.jsx(Popup$1.Title, {
4926
4991
  title: title,
4927
4992
  eventBus: eventBus,
4928
4993
  showCloseButton: true,
@@ -4940,14 +5005,13 @@ function FeelPopup$1(props) {
4940
5005
  }, index);
4941
5006
  })
4942
5007
  })
4943
- }), jsxRuntime.jsx(Popup.Body, {
5008
+ }), jsxRuntime.jsx(Popup$1.Body, {
4944
5009
  children: jsxRuntime.jsxs("div", {
4945
5010
  onKeyDownCapture: onKeyDownCapture,
4946
5011
  onKeyDown: onKeyDown,
4947
5012
  class: "bio-properties-panel-feel-popup__body",
4948
5013
  children: [type === 'feel' && jsxRuntime.jsx(FeelEditor, {
4949
5014
  enableGutters: true,
4950
- id: prefixId(entryId),
4951
5015
  name: entryId,
4952
5016
  onInput: onInput,
4953
5017
  value: value,
@@ -4956,7 +5020,6 @@ function FeelPopup$1(props) {
4956
5020
  ref: editorRef,
4957
5021
  tooltipContainer: tooltipContainer
4958
5022
  }), type === 'feelers' && jsxRuntime.jsx(TemplatingEditor, {
4959
- id: prefixId(entryId),
4960
5023
  contentAttributes: {
4961
5024
  'aria-label': title
4962
5025
  },
@@ -4973,14 +5036,17 @@ function FeelPopup$1(props) {
4973
5036
  })]
4974
5037
  });
4975
5038
  }
4976
- function prefixId(id) {
4977
- return `bio-properties-panel-${id}`;
4978
- }
4979
5039
  function autoCompletionOpen(element) {
4980
5040
  const editor = element.closest('.cm-editor');
4981
5041
  return editor ? editor.querySelector('.cm-tooltip-autocomplete') : null;
4982
5042
  }
4983
5043
 
5044
+ // while a snippet is active, Tab navigates its placeholders inside the editor
5045
+ function isSnippetNavigation(event) {
5046
+ const editor = event.target.closest('.cm-editor');
5047
+ return !!(editor && editor.querySelector('.cm-snippetField'));
5048
+ }
5049
+
4984
5050
  function getPopupTitle({
4985
5051
  element,
4986
5052
  label
@@ -5001,7 +5067,102 @@ function getPopupPosition() {
5001
5067
  }
5002
5068
 
5003
5069
  /**
5004
- * FEEL popup component, built as a singleton.
5070
+ * @typedef {Object} TextPopupProps
5071
+ * @property {string} entryId
5072
+ * @property {Function} onInput
5073
+ * @property {Function} onClose
5074
+ * @property {string} title
5075
+ * @property {string} value
5076
+ * @property {Object} [position]
5077
+ * @property {HTMLElement} [sourceElement]
5078
+ * @property {Object} [eventBus]
5079
+ */
5080
+
5081
+ const TEXT_POPUP_WIDTH = 700;
5082
+ const TEXT_POPUP_HEIGHT = 400;
5083
+
5084
+ /**
5085
+ * Plain text editor popup component.
5086
+ *
5087
+ * Intentionally kept separate from the FEEL popup: the two are expected to
5088
+ * diverge as we add FEEL-specific capabilities that do not apply to plain text.
5089
+ *
5090
+ * @param {TextPopupProps} props
5091
+ */
5092
+ function TextPopup(props) {
5093
+ const {
5094
+ entryId,
5095
+ onInput,
5096
+ onClose,
5097
+ title,
5098
+ value,
5099
+ position,
5100
+ sourceElement,
5101
+ eventBus
5102
+ } = props;
5103
+ const editorRef = hooks.useRef();
5104
+ const popupRef = hooks.useRef();
5105
+ const handleSetReturnFocus = () => {
5106
+ sourceElement && sourceElement.focus();
5107
+ };
5108
+ hooks.useEffect(() => {
5109
+ // set focus on editor when popup is opened
5110
+ if (editorRef.current) {
5111
+ editorRef.current.focus();
5112
+ }
5113
+ }, [editorRef]);
5114
+ return jsxRuntime.jsxs(Popup$1, {
5115
+ className: "bio-properties-panel-text-popup",
5116
+ position: position,
5117
+ title: title,
5118
+ returnFocus: false,
5119
+ closeOnEscape: true,
5120
+ delayInitialFocus: false,
5121
+ onClose: onClose,
5122
+ onPostDeactivate: handleSetReturnFocus,
5123
+ height: TEXT_POPUP_HEIGHT,
5124
+ width: TEXT_POPUP_WIDTH,
5125
+ ref: popupRef,
5126
+ children: [jsxRuntime.jsx(Popup$1.Title, {
5127
+ title: title,
5128
+ eventBus: eventBus,
5129
+ showCloseButton: true,
5130
+ closeButtonTooltip: "Save and close",
5131
+ onClose: onClose,
5132
+ draggable: true
5133
+ }), jsxRuntime.jsx(Popup$1.Body, {
5134
+ children: jsxRuntime.jsx("textarea", {
5135
+ id: prefixId(entryId),
5136
+ name: entryId,
5137
+ class: "bio-properties-panel-input",
5138
+ ref: editorRef,
5139
+ onInput: e => onInput(e.target.value),
5140
+ value: value || '',
5141
+ spellCheck: "false",
5142
+ autoComplete: "off",
5143
+ "aria-label": title,
5144
+ "data-gramm": "false"
5145
+ })
5146
+ })]
5147
+ });
5148
+ }
5149
+
5150
+ // The in-panel field stays mounted (hidden) while the popup is open, so we
5151
+ // scope the popup editor id to keep it unique in the DOM.
5152
+ function prefixId(id) {
5153
+ return `bio-properties-panel-popup-${id}`;
5154
+ }
5155
+
5156
+ const DEFAULT_POPUP_TYPE = 'text';
5157
+
5158
+ // consumers registering via #registerProvider default to DEFAULT_PRIORITY,
5159
+ // so their providers take precedence over the LOW_PRIORITY built-ins.
5160
+ const DEFAULT_PRIORITY = 1000;
5161
+ const LOW_PRIORITY = 500;
5162
+
5163
+ /**
5164
+ * Popup manager, built as a singleton. Renders the registered provider for a
5165
+ * given popup type; consumers may plug in their own via #registerProvider.
5005
5166
  *
5006
5167
  * In order to implement a custom replacement, handle the following events:
5007
5168
  * - `propertiesPanel.openPopup`
@@ -5014,16 +5175,19 @@ function getPopupPosition() {
5014
5175
  * - `feelPopup.close` - fired before the popup is unmounted. Event context contains the DOM node of the popup as `domNode`
5015
5176
  * - `feelPopup.closed` - fired after the popup is unmounted
5016
5177
  */
5017
- class FeelPopup {
5178
+ class Popup {
5018
5179
  constructor(eventBus, config = {}) {
5019
5180
  this._eventBus = eventBus;
5020
5181
  this._config = config;
5021
5182
  this._isOpen = false;
5022
- eventBus.on('propertiesPanel.openPopup', (_, context) => {
5023
- this.open(context.entryId, context, context.sourceElement);
5024
5183
 
5025
- // return true to indicate that popup was opened
5026
- return true;
5184
+ // built-in providers, registered at LOW_PRIORITY so consumers can
5185
+ // override them via #registerProvider using the default priority
5186
+ this.registerProvider('feel', LOW_PRIORITY, FeelPopup);
5187
+ this.registerProvider('feelers', LOW_PRIORITY, FeelPopup);
5188
+ this.registerProvider('text', LOW_PRIORITY, TextPopup);
5189
+ eventBus.on('propertiesPanel.openPopup', (_, context) => {
5190
+ return this.open(context.entryId, context, context.sourceElement);
5027
5191
  });
5028
5192
  eventBus.on(['propertiesPanel.closePopup', 'propertiesPanel.detach'], () => {
5029
5193
  this.close();
@@ -5031,7 +5195,43 @@ class FeelPopup {
5031
5195
  }
5032
5196
 
5033
5197
  /**
5034
- * Check if the FEEL popup is open.
5198
+ * Register a popup provider (component) for a given type.
5199
+ *
5200
+ * A higher `priority` wins when multiple providers are registered for the
5201
+ * same type; the built-in providers use a low priority so consumers override
5202
+ * them by default.
5203
+ *
5204
+ * @param {string} type
5205
+ * @param {number} [priority=DEFAULT_PRIORITY]
5206
+ * @param {Function|import('preact').Component} provider
5207
+ */
5208
+ registerProvider(type, priority, provider) {
5209
+ if (!provider) {
5210
+ provider = priority;
5211
+ priority = DEFAULT_PRIORITY;
5212
+ }
5213
+ this._eventBus.on('propertiesPanelPopup.getProviders.' + type, priority, function (event) {
5214
+ event.providers.push(provider);
5215
+ });
5216
+ }
5217
+
5218
+ /**
5219
+ * Get the popup providers registered for a type.
5220
+ *
5221
+ * @param {string} type
5222
+ * @return {Array<Function|import('preact').Component>}
5223
+ */
5224
+ _getProviders(type) {
5225
+ const event = this._eventBus.createEvent({
5226
+ type: 'propertiesPanelPopup.getProviders.' + type,
5227
+ providers: []
5228
+ });
5229
+ this._eventBus.fire(event);
5230
+ return event.providers;
5231
+ }
5232
+
5233
+ /**
5234
+ * Check if the popup is open.
5035
5235
  * @return {Boolean}
5036
5236
  */
5037
5237
  isOpen() {
@@ -5039,7 +5239,7 @@ class FeelPopup {
5039
5239
  }
5040
5240
 
5041
5241
  /**
5042
- * Open the FEEL popup.
5242
+ * Open the popup.
5043
5243
  *
5044
5244
  * @param {String} entryId
5045
5245
  * @param {Object} popupConfig
@@ -5048,7 +5248,7 @@ class FeelPopup {
5048
5248
  open(entryId, popupConfig, sourceElement) {
5049
5249
  // close before opening a new one
5050
5250
  this.close();
5051
- this._openPopup({
5251
+ return this._openPopup({
5052
5252
  ...popupConfig,
5053
5253
  entryId,
5054
5254
  sourceElement
@@ -5056,7 +5256,7 @@ class FeelPopup {
5056
5256
  }
5057
5257
 
5058
5258
  /**
5059
- * Close the FEEL popup.
5259
+ * Close the popup.
5060
5260
  */
5061
5261
  close() {
5062
5262
  this._closePopup();
@@ -5066,13 +5266,18 @@ class FeelPopup {
5066
5266
  element,
5067
5267
  label,
5068
5268
  sourceElement,
5069
- type
5269
+ type = DEFAULT_POPUP_TYPE
5070
5270
  } = context;
5271
+ const component = this._getProviders(type)[0];
5272
+ if (!component) {
5273
+ return false;
5274
+ }
5071
5275
  this._isOpen = true;
5072
5276
  this._eventBus.fire('propertiesPanelPopup.open', {
5073
5277
  container: this._config.feelPopupContainer,
5074
5278
  config: {
5075
5279
  ...context,
5280
+ component,
5076
5281
  links: this._config.getFeelPopupLinks?.(type) || [],
5077
5282
  onClose: () => {
5078
5283
  this._closePopup();
@@ -5089,6 +5294,7 @@ class FeelPopup {
5089
5294
  })
5090
5295
  }
5091
5296
  });
5297
+ return true;
5092
5298
  }
5093
5299
  _closePopup() {
5094
5300
  if (this._isOpen) {
@@ -5097,9 +5303,9 @@ class FeelPopup {
5097
5303
  }
5098
5304
  }
5099
5305
  }
5100
- FeelPopup.$inject = ['eventBus', 'config.propertiesPanel'];
5306
+ Popup.$inject = ['eventBus', 'config.propertiesPanel'];
5101
5307
 
5102
- class FeelPopupRenderer {
5308
+ class PopupRenderer {
5103
5309
  constructor(eventBus) {
5104
5310
  this._eventBus = eventBus;
5105
5311
  this._container = null;
@@ -5125,7 +5331,8 @@ class FeelPopupRenderer {
5125
5331
  // a custom renderer would have to use that context provider as well to have
5126
5332
  // access to the event bus and other services
5127
5333
  this._emit('feelPopup.open');
5128
- preact.render(jsxRuntime.jsx(FeelPopup$1, {
5334
+ const Component = config.component;
5335
+ preact.render(jsxRuntime.jsx(Component, {
5129
5336
  ...config,
5130
5337
  eventBus: this._eventBus
5131
5338
  }), element);
@@ -5149,7 +5356,7 @@ class FeelPopupRenderer {
5149
5356
  this._eventBus.fire(event, context);
5150
5357
  }
5151
5358
  }
5152
- FeelPopupRenderer.$inject = ['eventBus'];
5359
+ PopupRenderer.$inject = ['eventBus'];
5153
5360
 
5154
5361
  // helpers /////////////////
5155
5362
 
@@ -5167,8 +5374,8 @@ function getContainer(container) {
5167
5374
 
5168
5375
  var index = {
5169
5376
  __init__: ['feelPopup', 'feelPopupRenderer'],
5170
- feelPopup: ['type', FeelPopup],
5171
- feelPopupRenderer: ['type', FeelPopupRenderer]
5377
+ feelPopup: ['type', Popup],
5378
+ feelPopupRenderer: ['type', PopupRenderer]
5172
5379
  };
5173
5380
 
5174
5381
  exports.ArrowIcon = Arrow;