@bpmn-io/properties-panel 3.7.1 → 3.9.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,9 @@ 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;
1174
+ const containerNode = useMemo(() => getContainerNode(container), [container]);
1163
1175
  const handleKeydown = event => {
1164
1176
  // do not allow keyboard events to bubble
1165
1177
  event.stopPropagation();
@@ -1219,8 +1231,9 @@ function Popup(props) {
1219
1231
  class: classnames('bio-properties-panel-popup', className),
1220
1232
  style: style,
1221
1233
  children: props.children
1222
- }), container || document.body);
1234
+ }), containerNode || document.body);
1223
1235
  }
1236
+ const Popup = forwardRef(PopupComponent);
1224
1237
  Popup.Title = Title;
1225
1238
  Popup.Body = Body;
1226
1239
  Popup.Footer = Footer;
@@ -1229,6 +1242,7 @@ function Title(props) {
1229
1242
  children,
1230
1243
  className,
1231
1244
  draggable,
1245
+ emit = () => {},
1232
1246
  title,
1233
1247
  ...rest
1234
1248
  } = props;
@@ -1241,7 +1255,8 @@ function Title(props) {
1241
1255
  });
1242
1256
  const dragPreviewRef = useRef();
1243
1257
  const titleRef = useRef();
1244
- const onMove = throttle((_, delta) => {
1258
+ const onMove = (event, delta) => {
1259
+ cancel(event);
1245
1260
  const {
1246
1261
  x: dx,
1247
1262
  y: dy
@@ -1253,20 +1268,33 @@ function Title(props) {
1253
1268
  const popupParent = getPopupParent(titleRef.current);
1254
1269
  popupParent.style.top = newPosition.y + 'px';
1255
1270
  popupParent.style.left = newPosition.x + 'px';
1256
- });
1271
+
1272
+ // notify interested parties
1273
+ emit('dragover', {
1274
+ newPosition,
1275
+ delta
1276
+ });
1277
+ };
1257
1278
  const onMoveStart = event => {
1258
1279
  // initialize drag handler
1259
1280
  const onDragStart = createDragger(onMove, dragPreviewRef.current);
1260
1281
  onDragStart(event);
1282
+ event.stopPropagation();
1261
1283
  const popupParent = getPopupParent(titleRef.current);
1262
1284
  const bounds = popupParent.getBoundingClientRect();
1263
1285
  context.current.startPosition = {
1264
1286
  x: bounds.left,
1265
1287
  y: bounds.top
1266
1288
  };
1289
+
1290
+ // notify interested parties
1291
+ emit('dragstart');
1267
1292
  };
1268
1293
  const onMoveEnd = () => {
1269
1294
  context.current.newPosition = null;
1295
+
1296
+ // notify interested parties
1297
+ emit('dragend');
1270
1298
  };
1271
1299
  return jsxs("div", {
1272
1300
  class: classnames('bio-properties-panel-popup__header', draggable && 'draggable', className),
@@ -1319,12 +1347,26 @@ function Footer(props) {
1319
1347
  function getPopupParent(node) {
1320
1348
  return node.closest('.bio-properties-panel-popup');
1321
1349
  }
1350
+ function cancel(event) {
1351
+ event.preventDefault();
1352
+ event.stopPropagation();
1353
+ }
1354
+ function getContainerNode(node) {
1355
+ if (typeof node === 'string') {
1356
+ return query(node);
1357
+ }
1358
+ return node;
1359
+ }
1322
1360
 
1323
1361
  const FEEL_POPUP_WIDTH = 700;
1324
1362
  const FEEL_POPUP_HEIGHT = 250;
1325
1363
 
1326
1364
  /**
1327
- * FEEL popup component, built as a singleton.
1365
+ * FEEL popup component, built as a singleton. Emits lifecycle events as follows:
1366
+ * - `feelPopup.open` - fired before the popup is mounted
1367
+ * - `feelPopup.opened` - fired after the popup is mounted. Event context contains the DOM node of the popup
1368
+ * - `feelPopup.close` - fired before the popup is unmounted. Event context contains the DOM node of the popup
1369
+ * - `feelPopup.closed` - fired after the popup is unmounted
1328
1370
  */
1329
1371
  function FEELPopupRoot(props) {
1330
1372
  const {
@@ -1347,17 +1389,21 @@ function FEELPopupRoot(props) {
1347
1389
  const isOpen = useCallback(() => {
1348
1390
  return !!open;
1349
1391
  }, [open]);
1392
+ useUpdateEffect(() => {
1393
+ if (!open) {
1394
+ emit('closed');
1395
+ }
1396
+ }, [open]);
1350
1397
  const handleOpen = (entryId, config, _sourceElement) => {
1351
1398
  setSource(entryId);
1352
1399
  setPopupConfig(config);
1353
1400
  setOpen(true);
1354
1401
  setSourceElement(_sourceElement);
1355
- emit('opened');
1402
+ emit('open');
1356
1403
  };
1357
1404
  const handleClose = () => {
1358
1405
  setOpen(false);
1359
1406
  setSource(null);
1360
- emit('closed');
1361
1407
  };
1362
1408
  const feelPopupContext = {
1363
1409
  open: handleOpen,
@@ -1400,6 +1446,7 @@ function FEELPopupRoot(props) {
1400
1446
  onClose: handleClose,
1401
1447
  container: popupContainer,
1402
1448
  sourceElement: sourceElement,
1449
+ emit: emit,
1403
1450
  ...popupConfig
1404
1451
  }), props.children]
1405
1452
  });
@@ -1418,9 +1465,11 @@ function FeelPopupComponent(props) {
1418
1465
  tooltipContainer,
1419
1466
  type,
1420
1467
  value,
1421
- variables
1468
+ variables,
1469
+ emit
1422
1470
  } = props;
1423
1471
  const editorRef = useRef();
1472
+ const popupRef = useRef();
1424
1473
  const isAutoCompletionOpen = useRef(false);
1425
1474
  const handleSetReturnFocus = () => {
1426
1475
  sourceElement && sourceElement.focus();
@@ -1443,9 +1492,18 @@ function FeelPopupComponent(props) {
1443
1492
  }
1444
1493
  }
1445
1494
  };
1495
+ useEffect(() => {
1496
+ emit('opened', {
1497
+ domNode: popupRef.current
1498
+ });
1499
+ return () => emit('close', {
1500
+ domNode: popupRef.current
1501
+ });
1502
+ }, []);
1446
1503
  return jsxs(Popup, {
1447
1504
  container: container,
1448
1505
  className: "bio-properties-panel-feel-popup",
1506
+ emit: emit,
1449
1507
  position: position,
1450
1508
  title: title,
1451
1509
  onClose: onClose
@@ -1458,8 +1516,10 @@ function FeelPopupComponent(props) {
1458
1516
  onPostDeactivate: handleSetReturnFocus,
1459
1517
  height: FEEL_POPUP_HEIGHT,
1460
1518
  width: FEEL_POPUP_WIDTH,
1519
+ ref: popupRef,
1461
1520
  children: [jsx(Popup.Title, {
1462
1521
  title: title,
1522
+ emit: emit,
1463
1523
  draggable: true
1464
1524
  }), jsx(Popup.Body, {
1465
1525
  children: jsxs("div", {
@@ -1510,6 +1570,23 @@ function autoCompletionOpen(element) {
1510
1570
  return element.closest('.cm-editor').querySelector('.cm-tooltip-autocomplete');
1511
1571
  }
1512
1572
 
1573
+ /**
1574
+ * This hook behaves like useEffect, but does not trigger on the first render.
1575
+ *
1576
+ * @param {Function} effect
1577
+ * @param {Array} deps
1578
+ */
1579
+ function useUpdateEffect(effect, deps) {
1580
+ const isMounted = useRef(false);
1581
+ useEffect(() => {
1582
+ if (isMounted.current) {
1583
+ return effect();
1584
+ } else {
1585
+ isMounted.current = true;
1586
+ }
1587
+ }, deps);
1588
+ }
1589
+
1513
1590
  function ToggleSwitch(props) {
1514
1591
  const {
1515
1592
  id,