@vectoriox/iox-builder 2.10.2 → 2.10.3

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.
@@ -677,6 +677,65 @@ function arrayItemSchemaFields(properties, arrayPath) {
677
677
  return { fields: [], isPrimitive: true };
678
678
  }
679
679
 
680
+ const freshId = () => Math.random().toString(36).substr(2, 9);
681
+ /**
682
+ * Assign fresh ids to a cloned ComponentNode subtree AND remap intra-subtree references to the
683
+ * new ids. Used by every author-time clone path (duplicate, paste, block/external drop) so a copied
684
+ * structure stays self-consistent.
685
+ *
686
+ * Absolute node-id references that break under duplication if left unmapped:
687
+ * - `stateTriggers[state]` — the trigger-hover target (see style-engine.md → Targeted states)
688
+ * - interaction `action.target` / `reverseActions.target`
689
+ * A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
690
+ * intentionally still points at the external node.
691
+ *
692
+ * `styleId` strategy:
693
+ * - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
694
+ * - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
695
+ */
696
+ function regenerateNodeIds(root, opts = {}) {
697
+ const idMap = new Map();
698
+ const assign = (node) => {
699
+ const oldId = node.id;
700
+ node.id = freshId();
701
+ if (opts.styleId === 'regenerate') {
702
+ if (node.styleId)
703
+ node.styleId = freshId();
704
+ }
705
+ else {
706
+ node.styleId = undefined;
707
+ }
708
+ if (oldId)
709
+ idMap.set(oldId, node.id);
710
+ (node.children || []).forEach(assign);
711
+ };
712
+ assign(root);
713
+ remapNodeReferences(root, idMap);
714
+ }
715
+ /** Remap absolute node-id references inside a subtree to their new ids (per `idMap`). Ids not in
716
+ * the map (references to nodes outside the cloned subtree) are preserved. */
717
+ function remapNodeReferences(node, idMap) {
718
+ const triggers = node.stateTriggers;
719
+ if (triggers) {
720
+ for (const state of Object.keys(triggers)) {
721
+ const mapped = idMap.get(triggers[state]);
722
+ if (mapped)
723
+ triggers[state] = mapped;
724
+ }
725
+ }
726
+ for (const interaction of node.interactions || []) {
727
+ const actions = [...(interaction.actions || []), ...(interaction.reverseActions || [])];
728
+ for (const action of actions) {
729
+ if (action?.target && action.target !== 'self') {
730
+ const mapped = idMap.get(action.target);
731
+ if (mapped)
732
+ action.target = mapped;
733
+ }
734
+ }
735
+ }
736
+ (node.children || []).forEach(child => remapNodeReferences(child, idMap));
737
+ }
738
+
680
739
  /**
681
740
  * Locate a node within a forest (array of root nodes + their descendants) by
682
741
  * reference identity. Returns the parent array holding the node and its index,
@@ -1116,14 +1175,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
1116
1175
 
1117
1176
  function deepCloneNode(node) {
1118
1177
  const clone = JSON.parse(JSON.stringify(node));
1119
- regenerateIds(clone);
1178
+ // Fresh ids + remap intra-subtree references (stateTriggers, interaction targets) so a cloned
1179
+ // structure stays self-consistent — e.g. trigger-hover keeps pointing at the clone's own trigger.
1180
+ regenerateNodeIds(clone);
1120
1181
  return clone;
1121
1182
  }
1122
- function regenerateIds(node) {
1123
- node.id = Math.random().toString(36).substr(2, 9);
1124
- node.styleId = undefined;
1125
- (node.children || []).forEach(regenerateIds);
1126
- }
1127
1183
  class DragEngineService {
1128
1184
  constructor(registry, symbolRegistry) {
1129
1185
  this.registry = registry;
@@ -3365,6 +3421,27 @@ class RenderDirective {
3365
3421
  }
3366
3422
  return undefined;
3367
3423
  }
3424
+ /** Nearest ANCESTOR of the styled element that is a registered iox-node AND contains the trigger
3425
+ * element — i.e. the DOM equivalent of `nearestCommonAncestorId`, resolved per rendered instance.
3426
+ * Used to re-emit a `trigger-hover` rule after a move/duplicate. Returns undefined if the trigger
3427
+ * isn't in the DOM yet (caller then skips rather than clobbering the existing rule). */
3428
+ resolveTargetedScopeId(styledEl, triggerCssId) {
3429
+ let el = styledEl?.parentElement ?? null;
3430
+ while (el) {
3431
+ let cssId;
3432
+ for (let i = 0; i < el.classList.length; i++) {
3433
+ const c = el.classList[i];
3434
+ if (c.startsWith('iox-node-')) {
3435
+ cssId = c.slice('iox-node-'.length);
3436
+ break;
3437
+ }
3438
+ }
3439
+ if (cssId && el.querySelector(`.iox-node-${triggerCssId}`))
3440
+ return cssId;
3441
+ el = el.parentElement;
3442
+ }
3443
+ return undefined;
3444
+ }
3368
3445
  findParentNodeElement(target) {
3369
3446
  let el = target;
3370
3447
  while (el) {
@@ -3446,6 +3523,21 @@ class RenderDirective {
3446
3523
  continue;
3447
3524
  this.styleRegistry.upsert(cssId, composeVirtualTraits(stateMap, initialStyle), state, relativeToId);
3448
3525
  }
3526
+ else if (TARGETED_STATES.includes(state)) {
3527
+ // Targeted states (trigger-hover) compile to a `:has()` scoped to the nearest
3528
+ // common ancestor of the styled node + the chosen trigger. On a re-render (move,
3529
+ // duplicate, drop) we must RE-EMIT with a freshly-resolved scope — otherwise the
3530
+ // rule is clobbered and the hover stops working. Resolve the scope from the live
3531
+ // DOM (works per-instance); SKIP if the trigger isn't rendered yet, so we don't
3532
+ // clobber the correct rule page-ui's applyStateEnhancements already produced.
3533
+ const triggerCssId = node.stateTriggers?.[state];
3534
+ if (!triggerCssId)
3535
+ continue;
3536
+ const scopeId = this.resolveTargetedScopeId(childEl, triggerCssId);
3537
+ if (!scopeId)
3538
+ continue;
3539
+ this.styleRegistry.upsert(cssId, composeVirtualTraits(stateMap, initialStyle), state, scopeId, triggerCssId);
3540
+ }
3449
3541
  else {
3450
3542
  this.styleRegistry.upsert(cssId, composeVirtualTraits(stateMap, initialStyle), state);
3451
3543
  }
@@ -5294,15 +5386,11 @@ class BuilderComponent {
5294
5386
  }
5295
5387
  deepCloneWithNewIds(node) {
5296
5388
  const clone = JSON.parse(JSON.stringify(node));
5297
- this.assignNewIds(clone);
5389
+ // Fresh ids + remap intra-subtree references (stateTriggers, interaction targets) so a
5390
+ // duplicated/pasted structure stays self-consistent (trigger-hover keeps its own trigger).
5391
+ regenerateNodeIds(clone, { styleId: 'regenerate' });
5298
5392
  return clone;
5299
5393
  }
5300
- assignNewIds(node) {
5301
- node.id = Math.random().toString(36).slice(2, 10);
5302
- if (node.styleId)
5303
- node.styleId = Math.random().toString(36).slice(2, 10);
5304
- node.children?.forEach(child => this.assignNewIds(child));
5305
- }
5306
5394
  _stripGlobalFlags(node) {
5307
5395
  node.isGlobal = false;
5308
5396
  node.globalPosition = undefined;