@bpmn-io/form-js-editor 1.20.0 → 1.21.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.
@@ -304,6 +304,7 @@
304
304
  display: flex;
305
305
  flex-direction: column;
306
306
  padding: 20px;
307
+ gap: 4px;
307
308
  }
308
309
 
309
310
  .fjs-editor-container .fjs-drop-container-horizontal {
@@ -281,6 +281,7 @@
281
281
  display: flex;
282
282
  flex-direction: column;
283
283
  padding: 20px;
284
+ gap: 4px;
284
285
  }
285
286
 
286
287
  .fjs-editor-container .fjs-drop-container-horizontal {
package/dist/index.cjs CHANGED
@@ -1373,7 +1373,26 @@ const Slot = props => {
1373
1373
  const fillsAndSeparators = hooks.useMemo(() => {
1374
1374
  return buildFills(groups, fillRoot, separatorFn);
1375
1375
  }, [groups, fillRoot, separatorFn]);
1376
- return fillsAndSeparators;
1376
+
1377
+ // Framework-agnostic fills from SlotFillManager
1378
+ const editorContext = hooks.useContext(FormEditorContext);
1379
+ const slotFillManager = editorContext ? editorContext.getService('slotFillManager', false) : null;
1380
+ const eventBus = editorContext ? editorContext.getService('eventBus', false) : null;
1381
+ const [managerFills, setManagerFills] = hooks.useState([]);
1382
+ hooks.useEffect(() => {
1383
+ if (!eventBus || !slotFillManager) {
1384
+ return;
1385
+ }
1386
+ setManagerFills(slotFillManager.getFills(name));
1387
+ const onChange = () => setManagerFills(slotFillManager.getFills(name));
1388
+ eventBus.on('slotFillManager.changed', onChange);
1389
+ return () => eventBus.off('slotFillManager.changed', onChange);
1390
+ }, [eventBus, slotFillManager, name]);
1391
+ return jsxRuntime.jsxs(preact.Fragment, {
1392
+ children: [fillsAndSeparators, managerFills.map(fill => jsxRuntime.jsx(FillContainer, {
1393
+ fill: fill
1394
+ }, fill.fillId))]
1395
+ });
1377
1396
  };
1378
1397
 
1379
1398
  /**
@@ -1386,6 +1405,34 @@ const FillFragment = fill => jsxRuntime.jsx(preact.Fragment, {
1386
1405
  children: fill.children
1387
1406
  }, fill.id);
1388
1407
 
1408
+ /**
1409
+ * Mounts a single SlotFillManager fill's render callback into a DOM container.
1410
+ */
1411
+ function FillContainer({
1412
+ fill
1413
+ }) {
1414
+ const containerRef = hooks.useRef(null);
1415
+ const cleanupRef = hooks.useRef(null);
1416
+ hooks.useEffect(() => {
1417
+ const container = containerRef.current;
1418
+ if (!container) {
1419
+ return;
1420
+ }
1421
+ cleanupRef.current = fill.render(container) || null;
1422
+ return () => {
1423
+ if (typeof cleanupRef.current === 'function') {
1424
+ cleanupRef.current();
1425
+ cleanupRef.current = null;
1426
+ }
1427
+ container.innerHTML = '';
1428
+ };
1429
+ }, [fill]);
1430
+ return jsxRuntime.jsx("div", {
1431
+ ref: containerRef,
1432
+ "data-slot-fill": fill.fillId
1433
+ });
1434
+ }
1435
+
1389
1436
  /**
1390
1437
  * Creates an array of fills, with separators inserted between groups.
1391
1438
  *
@@ -2264,6 +2311,8 @@ function EmptyForm() {
2264
2311
  children: "Drag and drop components here to start designing."
2265
2312
  }), jsxRuntime.jsx("span", {
2266
2313
  children: "Use the preview window to test your form."
2314
+ }), jsxRuntime.jsx(Slot, {
2315
+ name: "editor-empty-state__footer"
2267
2316
  })]
2268
2317
  })
2269
2318
  });
@@ -14269,9 +14318,162 @@ class RenderInjector extends SectionModuleBase {
14269
14318
  }
14270
14319
  RenderInjector.$inject = ['eventBus'];
14271
14320
 
14321
+ /**
14322
+ * Framework-agnostic service for managing slot fills.
14323
+ *
14324
+ * Fills are registered as render callbacks: `(container: HTMLElement) => (() => void) | void`.
14325
+ * The optional return value is a cleanup function called when the fill is removed or the slot unmounts.
14326
+ *
14327
+ * @example
14328
+ *
14329
+ * // Via config (simplest):
14330
+ * new FormEditor({
14331
+ * slots: {
14332
+ * 'editor-empty-state__footer': (container) => {
14333
+ * container.textContent = 'Hello from vanilla JS';
14334
+ * }
14335
+ * }
14336
+ * });
14337
+ *
14338
+ * // Via config (multiple fills per slot):
14339
+ * new FormEditor({
14340
+ * slots: {
14341
+ * 'editor-empty-state__footer': [
14342
+ * (container) => { container.textContent = 'First'; },
14343
+ * { render: (container) => { container.textContent = 'Second'; }, priority: 10 }
14344
+ * ]
14345
+ * }
14346
+ * });
14347
+ *
14348
+ * // Via service (runtime):
14349
+ * const slotFillManager = editor.get('slotFillManager');
14350
+ * slotFillManager.addFill('editor-empty-state__footer', 'my-fill', {
14351
+ * render: (container) => { ... },
14352
+ * priority: 10,
14353
+ * group: 'custom'
14354
+ * });
14355
+ */
14356
+ class SlotFillManager {
14357
+ /**
14358
+ * @param {Object} slotsConfig
14359
+ * @param {import('../../core/EventBus').EventBus} eventBus
14360
+ */
14361
+ constructor(slotsConfig, eventBus) {
14362
+ this._eventBus = eventBus;
14363
+
14364
+ /** @type {Array<{ slotName: string, fillId: string, render: Function, priority: number, group: string }>} */
14365
+ this._fills = [];
14366
+ this._populateFromConfig(slotsConfig);
14367
+ }
14368
+
14369
+ /**
14370
+ * Register a fill for a named slot.
14371
+ *
14372
+ * @param {string} slotName - The slot to fill.
14373
+ * @param {string} fillId - Unique identifier for this fill.
14374
+ * @param {Function|Object} options - A render callback `(container) => cleanup`, or `{ render, priority?, group? }`.
14375
+ */
14376
+ addFill(slotName, fillId, options) {
14377
+ const fill = normalizeFill(slotName, fillId, options);
14378
+ this._fills = [...this._fills.filter(f => f.fillId !== fillId), fill];
14379
+ this._eventBus.fire('slotFillManager.changed');
14380
+ }
14381
+
14382
+ /**
14383
+ * Remove a fill by its ID.
14384
+ *
14385
+ * @param {string} fillId
14386
+ */
14387
+ removeFill(fillId) {
14388
+ const remaining = this._fills.filter(f => f.fillId !== fillId);
14389
+ if (remaining.length === this._fills.length) {
14390
+ return;
14391
+ }
14392
+ this._fills = remaining;
14393
+ this._eventBus.fire('slotFillManager.changed');
14394
+ }
14395
+
14396
+ /**
14397
+ * Get fills for a given slot, sorted by group (alphabetical) then priority (descending).
14398
+ *
14399
+ * @param {string} slotName
14400
+ * @returns {Array<{ slotName: string, fillId: string, render: Function, priority: number, group: string }>}
14401
+ */
14402
+ getFills(slotName) {
14403
+ const matching = this._fills.filter(f => f.slotName === slotName);
14404
+ return sortFills(matching);
14405
+ }
14406
+
14407
+ /**
14408
+ * @private
14409
+ */
14410
+ _populateFromConfig(slotsConfig) {
14411
+ Object.entries(slotsConfig || {}).forEach(([slotName, value]) => {
14412
+ if (Array.isArray(value)) {
14413
+ value.forEach((entry, index) => {
14414
+ this.addFill(slotName, `config__${slotName}_${index}`, entry);
14415
+ });
14416
+ } else {
14417
+ this.addFill(slotName, `config__${slotName}`, value);
14418
+ }
14419
+ });
14420
+ }
14421
+ }
14422
+ SlotFillManager.$inject = ['config.slots', 'eventBus'];
14423
+
14424
+ // helpers //////////
14425
+
14426
+ /**
14427
+ * @param {string} slotName
14428
+ * @param {string} fillId
14429
+ * @param {Function|Object} options
14430
+ * @returns {{ slotName: string, fillId: string, render: Function, priority: number, group: string }}
14431
+ */
14432
+ function normalizeFill(slotName, fillId, options) {
14433
+ if (typeof options === 'function') {
14434
+ return {
14435
+ slotName,
14436
+ fillId,
14437
+ render: options,
14438
+ priority: 0,
14439
+ group: 'z_default'
14440
+ };
14441
+ }
14442
+ const {
14443
+ render,
14444
+ priority = 0,
14445
+ group = 'z_default'
14446
+ } = options;
14447
+ return {
14448
+ slotName,
14449
+ fillId,
14450
+ render,
14451
+ priority,
14452
+ group
14453
+ };
14454
+ }
14455
+
14456
+ /**
14457
+ * Sort fills by group (alphabetical) then by priority (descending) within each group.
14458
+ */
14459
+ function sortFills(fills) {
14460
+ const grouped = groupBy(fills, f => f.group);
14461
+ return Object.keys(grouped).sort().flatMap(key => grouped[key].toSorted((a, b) => b.priority - a.priority));
14462
+ }
14463
+ function groupBy(items, keyFn) {
14464
+ return items.reduce((groups, item) => {
14465
+ const key = keyFn(item);
14466
+ return {
14467
+ ...groups,
14468
+ [key]: [...(groups[key] || []), item]
14469
+ };
14470
+ }, {});
14471
+ }
14472
+
14272
14473
  const RenderInjectionModule = {
14273
- __init__: ['renderInjector'],
14274
- renderInjector: ['type', RenderInjector]
14474
+ __init__: ['renderInjector', 'slotFillManager'],
14475
+ renderInjector: ['type', RenderInjector],
14476
+ slotFillManager: ['type', SlotFillManager]
14275
14477
  };
14276
14478
 
14277
14479
  var _path;