@vectoriox/iox-builder 1.4.20 → 1.4.22

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.
@@ -1088,24 +1088,57 @@ class InteractionEngineService {
1088
1088
  constructor(overlayService) {
1089
1089
  this.overlayService = overlayService;
1090
1090
  this.attached = new Map();
1091
+ /** Elements that have had pre-state applied (hidden by attach). */
1092
+ this.preStatedElements = new Set();
1093
+ /**
1094
+ * Pre-states queued for elements that weren't registered yet when the owner
1095
+ * node's attach() ran. Keyed by target node ID. Consumed when the target
1096
+ * element's own attach() fires.
1097
+ */
1098
+ this.pendingPreStates = new Map();
1099
+ /** Action types that animate an element from hidden → visible. */
1100
+ this.ENTRANCE_TYPES = new Set([
1101
+ 'fadeIn', 'moveUp', 'moveDown', 'moveLeft', 'moveRight', 'scaleIn', 'show',
1102
+ ]);
1103
+ /** Action types that animate an element from visible → hidden. */
1104
+ this.EXIT_TYPES = new Set(['fadeOut', 'scaleOut', 'hide']);
1091
1105
  }
1092
1106
  /** Wire all interactions for a node to its rendered DOM element. */
1093
1107
  attach(node) {
1094
- if (!node.interactions?.length)
1095
- return;
1096
1108
  const ref = this.overlayService.getNodeRef(node);
1097
1109
  if (!ref)
1098
1110
  return;
1111
+ // Apply any pre-states that were queued by other nodes that target THIS
1112
+ // element with an entrance animation but registered before we did.
1113
+ if (node.id) {
1114
+ const pending = this.pendingPreStates.get(node.id);
1115
+ if (pending) {
1116
+ for (const action of pending) {
1117
+ this.applyPreState(ref.element, action);
1118
+ }
1119
+ this.pendingPreStates.delete(node.id);
1120
+ }
1121
+ }
1122
+ if (!node.interactions?.length)
1123
+ return;
1099
1124
  const cleanups = [];
1100
1125
  for (const ix of node.interactions) {
1101
- // For entrance-type triggers, freeze the target at the animation's
1102
- // starting frame immediately so it is never briefly visible before
1103
- // the trigger fires (avoids the flash-then-animate flicker).
1104
- if (ix.trigger === 'pageLoad' || ix.trigger === 'viewportEnter') {
1105
- for (const action of ix.actions) {
1126
+ // Apply pre-state to targets of ANY entrance animation so the element
1127
+ // starts hidden regardless of whether the trigger is automatic (pageLoad,
1128
+ // viewportEnter) or user-driven (click).
1129
+ for (const action of ix.actions) {
1130
+ if (this.ENTRANCE_TYPES.has(action.type)) {
1106
1131
  const target = this.resolveTarget(node, action);
1107
- if (target)
1132
+ if (target) {
1108
1133
  this.applyPreState(target, action);
1134
+ }
1135
+ else if (action.target && action.target !== 'self') {
1136
+ // Target element not registered yet — queue the pre-state so
1137
+ // it gets applied the moment that element's attach() fires.
1138
+ const list = this.pendingPreStates.get(action.target) ?? [];
1139
+ list.push(action);
1140
+ this.pendingPreStates.set(action.target, list);
1141
+ }
1109
1142
  }
1110
1143
  }
1111
1144
  const cleanup = this.attachInteraction(node, ix, ref.element);
@@ -1127,9 +1160,34 @@ class InteractionEngineService {
1127
1160
  const ref = this.overlayService.getNodeRef(node);
1128
1161
  if (ref) {
1129
1162
  ref.element.getAnimations().forEach(a => a.cancel());
1130
- ref.element.style.removeProperty('opacity');
1131
- ref.element.style.removeProperty('transform');
1132
- ref.element.style.removeProperty('visibility');
1163
+ this.clearInlineAnimationStyles(ref.element);
1164
+ }
1165
+ // Also clean up pre-state on any target elements this node was animating.
1166
+ if (node.interactions) {
1167
+ for (const ix of node.interactions) {
1168
+ for (const action of ix.actions) {
1169
+ // Remove any pending pre-states for unregistered targets.
1170
+ if (action.target && action.target !== 'self') {
1171
+ const pending = this.pendingPreStates.get(action.target);
1172
+ if (pending) {
1173
+ const filtered = pending.filter(a => a !== action);
1174
+ if (filtered.length) {
1175
+ this.pendingPreStates.set(action.target, filtered);
1176
+ }
1177
+ else {
1178
+ this.pendingPreStates.delete(action.target);
1179
+ }
1180
+ }
1181
+ }
1182
+ // Clean up inline styles on already-registered target elements.
1183
+ const target = this.resolveTarget(node, action);
1184
+ if (target && this.preStatedElements.has(target)) {
1185
+ target.getAnimations().forEach(a => a.cancel());
1186
+ this.clearInlineAnimationStyles(target);
1187
+ this.preStatedElements.delete(target);
1188
+ }
1189
+ }
1190
+ }
1133
1191
  }
1134
1192
  }
1135
1193
  /** Re-attach interactions after they have been edited in the panel. */
@@ -1249,23 +1307,35 @@ class InteractionEngineService {
1249
1307
  if (!keyframes.length)
1250
1308
  return;
1251
1309
  // 'both' = backwards (holds first keyframe during delay) + forwards (holds last keyframe after end).
1252
- // This eliminates any flicker during the delay period.
1253
- element.animate(keyframes, {
1310
+ const anim = element.animate(keyframes, {
1254
1311
  duration: action.duration,
1255
1312
  delay: action.delay,
1256
1313
  easing: action.easing,
1257
1314
  fill: 'both',
1258
1315
  });
1316
+ if (this.ENTRANCE_TYPES.has(action.type)) {
1317
+ // Once the entrance animation finishes the element is fully visible —
1318
+ // restore pointer events so it can be interacted with.
1319
+ anim.finished.then(() => {
1320
+ element.style.removeProperty('pointer-events');
1321
+ }).catch(() => { });
1322
+ }
1323
+ else if (this.EXIT_TYPES.has(action.type)) {
1324
+ // Once the exit animation finishes the element is invisible —
1325
+ // disable pointer events so it doesn't silently block clicks underneath it.
1326
+ anim.finished.then(() => {
1327
+ element.style.setProperty('pointer-events', 'none');
1328
+ }).catch(() => { });
1329
+ }
1259
1330
  }
1260
1331
  /**
1261
1332
  * Freeze an element at the animation's starting frame immediately, before any
1262
- * trigger fires. Without this, entrance animations (fadeIn, moveUp, …) show the
1263
- * element at its natural visible state for the time between mount and trigger —
1264
- * causing a visible-then-disappear-then-animate flicker.
1333
+ * trigger fires. Called for ALL entrance-type actions regardless of trigger so
1334
+ * that a click-triggered fadeIn on a menu overlay also starts it hidden.
1265
1335
  *
1266
- * Only call this for triggers that will eventually animate the element in
1267
- * (pageLoad, viewportEnter). Click/hover triggers must NOT pre-hide the element
1268
- * because the element should remain normally visible until interacted with.
1336
+ * Also sets pointer-events:none so the invisible element does not block clicks
1337
+ * on other content beneath it. Pointer events are restored in executeAction()
1338
+ * once the entrance animation finishes.
1269
1339
  */
1270
1340
  applyPreState(element, action) {
1271
1341
  const { keyframes } = this.buildAnimation(action);
@@ -1275,6 +1345,18 @@ class InteractionEngineService {
1275
1345
  for (const [prop, val] of Object.entries(first)) {
1276
1346
  element.style[prop] = String(val);
1277
1347
  }
1348
+ // Invisible element must not intercept pointer events.
1349
+ const opacity = first['opacity'];
1350
+ if (opacity === '0' || opacity === 0) {
1351
+ element.style.setProperty('pointer-events', 'none');
1352
+ }
1353
+ this.preStatedElements.add(element);
1354
+ }
1355
+ clearInlineAnimationStyles(element) {
1356
+ element.style.removeProperty('opacity');
1357
+ element.style.removeProperty('transform');
1358
+ element.style.removeProperty('visibility');
1359
+ element.style.removeProperty('pointer-events');
1278
1360
  }
1279
1361
  reverseAction(element, action) {
1280
1362
  const { keyframes } = this.buildAnimation(action);