@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.
package/dist/index.es.js CHANGED
@@ -1353,7 +1353,26 @@ const Slot = props => {
1353
1353
  const fillsAndSeparators = useMemo(() => {
1354
1354
  return buildFills(groups, fillRoot, separatorFn);
1355
1355
  }, [groups, fillRoot, separatorFn]);
1356
- return fillsAndSeparators;
1356
+
1357
+ // Framework-agnostic fills from SlotFillManager
1358
+ const editorContext = useContext(FormEditorContext);
1359
+ const slotFillManager = editorContext ? editorContext.getService('slotFillManager', false) : null;
1360
+ const eventBus = editorContext ? editorContext.getService('eventBus', false) : null;
1361
+ const [managerFills, setManagerFills] = useState([]);
1362
+ useEffect(() => {
1363
+ if (!eventBus || !slotFillManager) {
1364
+ return;
1365
+ }
1366
+ setManagerFills(slotFillManager.getFills(name));
1367
+ const onChange = () => setManagerFills(slotFillManager.getFills(name));
1368
+ eventBus.on('slotFillManager.changed', onChange);
1369
+ return () => eventBus.off('slotFillManager.changed', onChange);
1370
+ }, [eventBus, slotFillManager, name]);
1371
+ return jsxs(Fragment, {
1372
+ children: [fillsAndSeparators, managerFills.map(fill => jsx(FillContainer, {
1373
+ fill: fill
1374
+ }, fill.fillId))]
1375
+ });
1357
1376
  };
1358
1377
 
1359
1378
  /**
@@ -1366,6 +1385,34 @@ const FillFragment = fill => jsx(Fragment, {
1366
1385
  children: fill.children
1367
1386
  }, fill.id);
1368
1387
 
1388
+ /**
1389
+ * Mounts a single SlotFillManager fill's render callback into a DOM container.
1390
+ */
1391
+ function FillContainer({
1392
+ fill
1393
+ }) {
1394
+ const containerRef = useRef(null);
1395
+ const cleanupRef = useRef(null);
1396
+ useEffect(() => {
1397
+ const container = containerRef.current;
1398
+ if (!container) {
1399
+ return;
1400
+ }
1401
+ cleanupRef.current = fill.render(container) || null;
1402
+ return () => {
1403
+ if (typeof cleanupRef.current === 'function') {
1404
+ cleanupRef.current();
1405
+ cleanupRef.current = null;
1406
+ }
1407
+ container.innerHTML = '';
1408
+ };
1409
+ }, [fill]);
1410
+ return jsx("div", {
1411
+ ref: containerRef,
1412
+ "data-slot-fill": fill.fillId
1413
+ });
1414
+ }
1415
+
1369
1416
  /**
1370
1417
  * Creates an array of fills, with separators inserted between groups.
1371
1418
  *
@@ -2244,6 +2291,8 @@ function EmptyForm() {
2244
2291
  children: "Drag and drop components here to start designing."
2245
2292
  }), jsx("span", {
2246
2293
  children: "Use the preview window to test your form."
2294
+ }), jsx(Slot, {
2295
+ name: "editor-empty-state__footer"
2247
2296
  })]
2248
2297
  })
2249
2298
  });
@@ -14249,9 +14298,162 @@ class RenderInjector extends SectionModuleBase {
14249
14298
  }
14250
14299
  RenderInjector.$inject = ['eventBus'];
14251
14300
 
14301
+ /**
14302
+ * Framework-agnostic service for managing slot fills.
14303
+ *
14304
+ * Fills are registered as render callbacks: `(container: HTMLElement) => (() => void) | void`.
14305
+ * The optional return value is a cleanup function called when the fill is removed or the slot unmounts.
14306
+ *
14307
+ * @example
14308
+ *
14309
+ * // Via config (simplest):
14310
+ * new FormEditor({
14311
+ * slots: {
14312
+ * 'editor-empty-state__footer': (container) => {
14313
+ * container.textContent = 'Hello from vanilla JS';
14314
+ * }
14315
+ * }
14316
+ * });
14317
+ *
14318
+ * // Via config (multiple fills per slot):
14319
+ * new FormEditor({
14320
+ * slots: {
14321
+ * 'editor-empty-state__footer': [
14322
+ * (container) => { container.textContent = 'First'; },
14323
+ * { render: (container) => { container.textContent = 'Second'; }, priority: 10 }
14324
+ * ]
14325
+ * }
14326
+ * });
14327
+ *
14328
+ * // Via service (runtime):
14329
+ * const slotFillManager = editor.get('slotFillManager');
14330
+ * slotFillManager.addFill('editor-empty-state__footer', 'my-fill', {
14331
+ * render: (container) => { ... },
14332
+ * priority: 10,
14333
+ * group: 'custom'
14334
+ * });
14335
+ */
14336
+ class SlotFillManager {
14337
+ /**
14338
+ * @param {Object} slotsConfig
14339
+ * @param {import('../../core/EventBus').EventBus} eventBus
14340
+ */
14341
+ constructor(slotsConfig, eventBus) {
14342
+ this._eventBus = eventBus;
14343
+
14344
+ /** @type {Array<{ slotName: string, fillId: string, render: Function, priority: number, group: string }>} */
14345
+ this._fills = [];
14346
+ this._populateFromConfig(slotsConfig);
14347
+ }
14348
+
14349
+ /**
14350
+ * Register a fill for a named slot.
14351
+ *
14352
+ * @param {string} slotName - The slot to fill.
14353
+ * @param {string} fillId - Unique identifier for this fill.
14354
+ * @param {Function|Object} options - A render callback `(container) => cleanup`, or `{ render, priority?, group? }`.
14355
+ */
14356
+ addFill(slotName, fillId, options) {
14357
+ const fill = normalizeFill(slotName, fillId, options);
14358
+ this._fills = [...this._fills.filter(f => f.fillId !== fillId), fill];
14359
+ this._eventBus.fire('slotFillManager.changed');
14360
+ }
14361
+
14362
+ /**
14363
+ * Remove a fill by its ID.
14364
+ *
14365
+ * @param {string} fillId
14366
+ */
14367
+ removeFill(fillId) {
14368
+ const remaining = this._fills.filter(f => f.fillId !== fillId);
14369
+ if (remaining.length === this._fills.length) {
14370
+ return;
14371
+ }
14372
+ this._fills = remaining;
14373
+ this._eventBus.fire('slotFillManager.changed');
14374
+ }
14375
+
14376
+ /**
14377
+ * Get fills for a given slot, sorted by group (alphabetical) then priority (descending).
14378
+ *
14379
+ * @param {string} slotName
14380
+ * @returns {Array<{ slotName: string, fillId: string, render: Function, priority: number, group: string }>}
14381
+ */
14382
+ getFills(slotName) {
14383
+ const matching = this._fills.filter(f => f.slotName === slotName);
14384
+ return sortFills(matching);
14385
+ }
14386
+
14387
+ /**
14388
+ * @private
14389
+ */
14390
+ _populateFromConfig(slotsConfig) {
14391
+ Object.entries(slotsConfig || {}).forEach(([slotName, value]) => {
14392
+ if (Array.isArray(value)) {
14393
+ value.forEach((entry, index) => {
14394
+ this.addFill(slotName, `config__${slotName}_${index}`, entry);
14395
+ });
14396
+ } else {
14397
+ this.addFill(slotName, `config__${slotName}`, value);
14398
+ }
14399
+ });
14400
+ }
14401
+ }
14402
+ SlotFillManager.$inject = ['config.slots', 'eventBus'];
14403
+
14404
+ // helpers //////////
14405
+
14406
+ /**
14407
+ * @param {string} slotName
14408
+ * @param {string} fillId
14409
+ * @param {Function|Object} options
14410
+ * @returns {{ slotName: string, fillId: string, render: Function, priority: number, group: string }}
14411
+ */
14412
+ function normalizeFill(slotName, fillId, options) {
14413
+ if (typeof options === 'function') {
14414
+ return {
14415
+ slotName,
14416
+ fillId,
14417
+ render: options,
14418
+ priority: 0,
14419
+ group: 'z_default'
14420
+ };
14421
+ }
14422
+ const {
14423
+ render,
14424
+ priority = 0,
14425
+ group = 'z_default'
14426
+ } = options;
14427
+ return {
14428
+ slotName,
14429
+ fillId,
14430
+ render,
14431
+ priority,
14432
+ group
14433
+ };
14434
+ }
14435
+
14436
+ /**
14437
+ * Sort fills by group (alphabetical) then by priority (descending) within each group.
14438
+ */
14439
+ function sortFills(fills) {
14440
+ const grouped = groupBy(fills, f => f.group);
14441
+ return Object.keys(grouped).sort().flatMap(key => grouped[key].toSorted((a, b) => b.priority - a.priority));
14442
+ }
14443
+ function groupBy(items, keyFn) {
14444
+ return items.reduce((groups, item) => {
14445
+ const key = keyFn(item);
14446
+ return {
14447
+ ...groups,
14448
+ [key]: [...(groups[key] || []), item]
14449
+ };
14450
+ }, {});
14451
+ }
14452
+
14252
14453
  const RenderInjectionModule = {
14253
- __init__: ['renderInjector'],
14254
- renderInjector: ['type', RenderInjector]
14454
+ __init__: ['renderInjector', 'slotFillManager'],
14455
+ renderInjector: ['type', RenderInjector],
14456
+ slotFillManager: ['type', SlotFillManager]
14255
14457
  };
14256
14458
 
14257
14459
  var _path;