@bpmn-io/properties-panel 3.7.0 → 3.8.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.
@@ -1237,7 +1237,7 @@ textarea.bio-properties-panel-input {
1237
1237
  --feel-popup-close-background-color: hsla(219, 99%, 53%, 1);
1238
1238
  --feel-popup-gutters-background-color: hsla(0, 0%, 90%, 1);
1239
1239
 
1240
- position: absolute;
1240
+ position: fixed;
1241
1241
  display: flex;
1242
1242
  flex: auto;
1243
1243
  flex-direction: column;
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useContext, useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } from '../preact/hooks';
2
- import { isFunction, throttle, isString, isArray, get, assign, set, sortBy, find, isNumber, debounce } from 'min-dash';
2
+ import { isFunction, isString, isArray, get, assign, set, sortBy, find, isNumber, debounce } from 'min-dash';
3
3
  import { createPortal, forwardRef } from '../preact/compat';
4
4
  import { jsx, jsxs, Fragment } from '../preact/jsx-runtime';
5
5
  import { createContext, createElement } from '../preact';
@@ -649,20 +649,24 @@ function Group(props) {
649
649
 
650
650
  // set edited state depending on all entries
651
651
  useEffect(() => {
652
- const hasOneEditedEntry = entries.find(entry => {
653
- const {
654
- id,
655
- isEdited
656
- } = entry;
657
- const entryNode = query(`[data-entry-id="${id}"]`);
658
- if (!isFunction(isEdited) || !entryNode) {
659
- return false;
660
- }
661
- const inputNode = query('.bio-properties-panel-input', entryNode);
662
- return isEdited(inputNode);
652
+ // TODO(@barmac): replace with CSS when `:has()` is supported in all major browsers, or rewrite as in https://github.com/camunda/camunda-modeler/issues/3815#issuecomment-1733038161
653
+ const scheduled = requestAnimationFrame(() => {
654
+ const hasOneEditedEntry = entries.find(entry => {
655
+ const {
656
+ id,
657
+ isEdited
658
+ } = entry;
659
+ const entryNode = query(`[data-entry-id="${id}"]`);
660
+ if (!isFunction(isEdited) || !entryNode) {
661
+ return false;
662
+ }
663
+ const inputNode = query('.bio-properties-panel-input', entryNode);
664
+ return isEdited(inputNode);
665
+ });
666
+ setEdited(hasOneEditedEntry);
663
667
  });
664
- setEdited(hasOneEditedEntry);
665
- }, [entries]);
668
+ return () => cancelAnimationFrame(scheduled);
669
+ }, [entries, setEdited]);
666
670
 
667
671
  // set error state depending on all entries
668
672
  const allErrors = useErrors();
@@ -1096,7 +1100,11 @@ function createDragger(fn, dragPreview) {
1096
1100
  // (2) setup drag listeners
1097
1101
 
1098
1102
  // attach drag + cleanup event
1099
- document.addEventListener('dragover', onDrag);
1103
+ // we need to do this to make sure we track cursor
1104
+ // movements before we reach other drag event handlers,
1105
+ // e.g. in child containers.
1106
+ document.addEventListener('dragover', onDrag, true);
1107
+ document.addEventListener('dragenter', preventDefault, true);
1100
1108
  document.addEventListener('dragend', onEnd);
1101
1109
  document.addEventListener('drop', preventDefault);
1102
1110
  }
@@ -1110,7 +1118,8 @@ function createDragger(fn, dragPreview) {
1110
1118
  return fn.call(self, event, delta);
1111
1119
  }
1112
1120
  function onEnd() {
1113
- document.removeEventListener('dragover', onDrag);
1121
+ document.removeEventListener('dragover', onDrag, true);
1122
+ document.removeEventListener('dragenter', preventDefault, true);
1114
1123
  document.removeEventListener('dragend', onEnd);
1115
1124
  document.removeEventListener('drop', preventDefault);
1116
1125
  }
@@ -1142,8 +1151,9 @@ const noop$3 = () => {};
1142
1151
  * @param {boolean} [props.returnFocus]
1143
1152
  * @param {boolean} [props.closeOnEscape]
1144
1153
  * @param {string} props.title
1154
+ * @param {Ref} [ref]
1145
1155
  */
1146
- function Popup(props) {
1156
+ function PopupComponent(props, globalRef) {
1147
1157
  const {
1148
1158
  container,
1149
1159
  className,
@@ -1159,7 +1169,8 @@ function Popup(props) {
1159
1169
  title
1160
1170
  } = props;
1161
1171
  const focusTrapRef = useRef(null);
1162
- const popupRef = useRef(null);
1172
+ const localRef = useRef(null);
1173
+ const popupRef = globalRef || localRef;
1163
1174
  const handleKeydown = event => {
1164
1175
  // do not allow keyboard events to bubble
1165
1176
  event.stopPropagation();
@@ -1221,6 +1232,7 @@ function Popup(props) {
1221
1232
  children: props.children
1222
1233
  }), container || document.body);
1223
1234
  }
1235
+ const Popup = forwardRef(PopupComponent);
1224
1236
  Popup.Title = Title;
1225
1237
  Popup.Body = Body;
1226
1238
  Popup.Footer = Footer;
@@ -1229,6 +1241,7 @@ function Title(props) {
1229
1241
  children,
1230
1242
  className,
1231
1243
  draggable,
1244
+ emit = () => {},
1232
1245
  title,
1233
1246
  ...rest
1234
1247
  } = props;
@@ -1241,7 +1254,8 @@ function Title(props) {
1241
1254
  });
1242
1255
  const dragPreviewRef = useRef();
1243
1256
  const titleRef = useRef();
1244
- const onMove = throttle((_, delta) => {
1257
+ const onMove = (event, delta) => {
1258
+ cancel(event);
1245
1259
  const {
1246
1260
  x: dx,
1247
1261
  y: dy
@@ -1253,20 +1267,33 @@ function Title(props) {
1253
1267
  const popupParent = getPopupParent(titleRef.current);
1254
1268
  popupParent.style.top = newPosition.y + 'px';
1255
1269
  popupParent.style.left = newPosition.x + 'px';
1256
- });
1270
+
1271
+ // notify interested parties
1272
+ emit('dragover', {
1273
+ newPosition,
1274
+ delta
1275
+ });
1276
+ };
1257
1277
  const onMoveStart = event => {
1258
1278
  // initialize drag handler
1259
1279
  const onDragStart = createDragger(onMove, dragPreviewRef.current);
1260
1280
  onDragStart(event);
1281
+ event.stopPropagation();
1261
1282
  const popupParent = getPopupParent(titleRef.current);
1262
1283
  const bounds = popupParent.getBoundingClientRect();
1263
1284
  context.current.startPosition = {
1264
1285
  x: bounds.left,
1265
1286
  y: bounds.top
1266
1287
  };
1288
+
1289
+ // notify interested parties
1290
+ emit('dragstart');
1267
1291
  };
1268
1292
  const onMoveEnd = () => {
1269
1293
  context.current.newPosition = null;
1294
+
1295
+ // notify interested parties
1296
+ emit('dragend');
1270
1297
  };
1271
1298
  return jsxs("div", {
1272
1299
  class: classnames('bio-properties-panel-popup__header', draggable && 'draggable', className),
@@ -1319,12 +1346,20 @@ function Footer(props) {
1319
1346
  function getPopupParent(node) {
1320
1347
  return node.closest('.bio-properties-panel-popup');
1321
1348
  }
1349
+ function cancel(event) {
1350
+ event.preventDefault();
1351
+ event.stopPropagation();
1352
+ }
1322
1353
 
1323
1354
  const FEEL_POPUP_WIDTH = 700;
1324
1355
  const FEEL_POPUP_HEIGHT = 250;
1325
1356
 
1326
1357
  /**
1327
- * FEEL popup component, built as a singleton.
1358
+ * FEEL popup component, built as a singleton. Emits lifecycle events as follows:
1359
+ * - `feelPopup.open` - fired before the popup is mounted
1360
+ * - `feelPopup.opened` - fired after the popup is mounted. Event context contains the DOM node of the popup
1361
+ * - `feelPopup.close` - fired before the popup is unmounted. Event context contains the DOM node of the popup
1362
+ * - `feelPopup.closed` - fired after the popup is unmounted
1328
1363
  */
1329
1364
  function FEELPopupRoot(props) {
1330
1365
  const {
@@ -1347,17 +1382,21 @@ function FEELPopupRoot(props) {
1347
1382
  const isOpen = useCallback(() => {
1348
1383
  return !!open;
1349
1384
  }, [open]);
1385
+ useUpdateEffect(() => {
1386
+ if (!open) {
1387
+ emit('closed');
1388
+ }
1389
+ }, [open]);
1350
1390
  const handleOpen = (entryId, config, _sourceElement) => {
1351
1391
  setSource(entryId);
1352
1392
  setPopupConfig(config);
1353
1393
  setOpen(true);
1354
1394
  setSourceElement(_sourceElement);
1355
- emit('opened');
1395
+ emit('open');
1356
1396
  };
1357
1397
  const handleClose = () => {
1358
1398
  setOpen(false);
1359
1399
  setSource(null);
1360
- emit('closed');
1361
1400
  };
1362
1401
  const feelPopupContext = {
1363
1402
  open: handleOpen,
@@ -1400,6 +1439,7 @@ function FEELPopupRoot(props) {
1400
1439
  onClose: handleClose,
1401
1440
  container: popupContainer,
1402
1441
  sourceElement: sourceElement,
1442
+ emit: emit,
1403
1443
  ...popupConfig
1404
1444
  }), props.children]
1405
1445
  });
@@ -1418,9 +1458,11 @@ function FeelPopupComponent(props) {
1418
1458
  tooltipContainer,
1419
1459
  type,
1420
1460
  value,
1421
- variables
1461
+ variables,
1462
+ emit
1422
1463
  } = props;
1423
1464
  const editorRef = useRef();
1465
+ const popupRef = useRef();
1424
1466
  const isAutoCompletionOpen = useRef(false);
1425
1467
  const handleSetReturnFocus = () => {
1426
1468
  sourceElement && sourceElement.focus();
@@ -1443,9 +1485,18 @@ function FeelPopupComponent(props) {
1443
1485
  }
1444
1486
  }
1445
1487
  };
1488
+ useEffect(() => {
1489
+ emit('opened', {
1490
+ domNode: popupRef.current
1491
+ });
1492
+ return () => emit('close', {
1493
+ domNode: popupRef.current
1494
+ });
1495
+ }, []);
1446
1496
  return jsxs(Popup, {
1447
1497
  container: container,
1448
1498
  className: "bio-properties-panel-feel-popup",
1499
+ emit: emit,
1449
1500
  position: position,
1450
1501
  title: title,
1451
1502
  onClose: onClose
@@ -1458,8 +1509,10 @@ function FeelPopupComponent(props) {
1458
1509
  onPostDeactivate: handleSetReturnFocus,
1459
1510
  height: FEEL_POPUP_HEIGHT,
1460
1511
  width: FEEL_POPUP_WIDTH,
1512
+ ref: popupRef,
1461
1513
  children: [jsx(Popup.Title, {
1462
1514
  title: title,
1515
+ emit: emit,
1463
1516
  draggable: true
1464
1517
  }), jsx(Popup.Body, {
1465
1518
  children: jsxs("div", {
@@ -1510,6 +1563,23 @@ function autoCompletionOpen(element) {
1510
1563
  return element.closest('.cm-editor').querySelector('.cm-tooltip-autocomplete');
1511
1564
  }
1512
1565
 
1566
+ /**
1567
+ * This hook behaves like useEffect, but does not trigger on the first render.
1568
+ *
1569
+ * @param {Function} effect
1570
+ * @param {Array} deps
1571
+ */
1572
+ function useUpdateEffect(effect, deps) {
1573
+ const isMounted = useRef(false);
1574
+ useEffect(() => {
1575
+ if (isMounted.current) {
1576
+ return effect();
1577
+ } else {
1578
+ isMounted.current = true;
1579
+ }
1580
+ }, deps);
1581
+ }
1582
+
1513
1583
  function ToggleSwitch(props) {
1514
1584
  const {
1515
1585
  id,
@@ -2461,7 +2531,7 @@ function calculatePopupPosition(element) {
2461
2531
 
2462
2532
  // todo(pinussilvestrus): make this configurable in the future
2463
2533
  function getPopupTitle(element, label) {
2464
- let popupTitle;
2534
+ let popupTitle = '';
2465
2535
  if (element && element.type) {
2466
2536
  popupTitle = `${element.type} / `;
2467
2537
  }