@vectoriox/iox-builder 1.4.71 → 1.4.73

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.
@@ -209,6 +209,7 @@ class OverlayService {
209
209
  this.select$ = this.selectSubject.asObservable();
210
210
  this.containerElement = null;
211
211
  this.scrollContainerElement = null;
212
+ this.intersectionRootElement = null;
212
213
  /** Emits the scroll container element whenever it is registered. */
213
214
  this.scrollContainer$ = new Subject();
214
215
  /** Emits cursor coords when a canvas element is right-clicked. */
@@ -232,6 +233,13 @@ class OverlayService {
232
233
  getScrollContainer() {
233
234
  return this.scrollContainerElement;
234
235
  }
236
+ /** The clipping container used as IntersectionObserver root in builder mode (.preview-scroll). */
237
+ setIntersectionRoot(el) {
238
+ this.intersectionRootElement = el;
239
+ }
240
+ getIntersectionRoot() {
241
+ return this.intersectionRootElement;
242
+ }
235
243
  /**
236
244
  * Resolve the inner styled element.
237
245
  * Builder components apply [ngStyle] on their first child element,
@@ -1258,12 +1266,12 @@ class InteractionEngineService {
1258
1266
  const cleanups = [];
1259
1267
  for (const ix of node.interactions) {
1260
1268
  // Pre-state: hide target elements before their entrance animation fires.
1261
- // In builder mode: skip pre-state for all auto-fire triggers (pageLoad,
1262
- // viewportEnter, scrollProgress) — they would stay permanently hidden if
1263
- // the trigger is suppressed or the canvas scroll container doesn't expose
1264
- // a standard browser viewport for IntersectionObserver.
1265
- // Click/hover triggers are always active so their pre-state is applied.
1266
- const skipPreState = inBuilder && ix.trigger !== 'click' && ix.trigger !== 'hover';
1269
+ // In builder mode: skip pre-state only for pageLoad and scrollProgress —
1270
+ // those triggers are suppressed in the canvas so elements would stay hidden.
1271
+ // viewportEnter uses IntersectionObserver with .preview-scroll as its root,
1272
+ // so it fires correctly when the canvas scrolls — pre-state is safe to apply.
1273
+ // Click/hover triggers are always active so their pre-state is applied too.
1274
+ const skipPreState = inBuilder && (ix.trigger === 'pageLoad' || ix.trigger === 'scrollProgress');
1267
1275
  if (!skipPreState) {
1268
1276
  for (const action of ix.actions) {
1269
1277
  if (this.ENTRANCE_TYPES.has(action.type)) {
@@ -1423,25 +1431,56 @@ class InteractionEngineService {
1423
1431
  }
1424
1432
  attachViewportEnter(node, ix, element) {
1425
1433
  const firedSet = new Set();
1426
- const observer = new IntersectionObserver((entries) => {
1427
- for (const entry of entries) {
1428
- if (entry.isIntersecting) {
1429
- const key = ix.id;
1434
+ const fire = () => {
1435
+ const key = ix.id;
1436
+ if (ix.actions.some(a => a.once) && firedSet.has(key))
1437
+ return;
1438
+ if (this.hasPreStatedAncestor(element)) {
1439
+ this.deferUntilVisible(element, () => {
1430
1440
  if (ix.actions.some(a => a.once) && firedSet.has(key))
1431
- continue;
1432
- // Element is in viewport but may be inside a still-fading parent.
1433
- // Defer until all pre-stated ancestors finish their entrance animation.
1434
- if (this.hasPreStatedAncestor(element)) {
1435
- this.deferUntilVisible(element, () => {
1436
- if (ix.actions.some(a => a.once) && firedSet.has(key))
1437
- return;
1438
- firedSet.add(key);
1439
- this.runActions(node, ix.actions);
1440
- });
1441
- continue;
1442
- }
1441
+ return;
1443
1442
  firedSet.add(key);
1444
1443
  this.runActions(node, ix.actions);
1444
+ });
1445
+ return;
1446
+ }
1447
+ firedSet.add(key);
1448
+ this.runActions(node, ix.actions);
1449
+ };
1450
+ const canvasRoot = this.overlayService.getIntersectionRoot();
1451
+ if (canvasRoot) {
1452
+ // Builder mode: IntersectionObserver with a scaled/transformed canvas root is unreliable.
1453
+ // Use getBoundingClientRect against the canvas clipping container instead.
1454
+ const isInCanvasViewport = () => {
1455
+ const rootRect = canvasRoot.getBoundingClientRect();
1456
+ const elRect = element.getBoundingClientRect();
1457
+ const overlapH = Math.min(elRect.bottom, rootRect.bottom) - Math.max(elRect.top, rootRect.top);
1458
+ const elH = elRect.bottom - elRect.top;
1459
+ return elH > 0 && overlapH / elH >= 0.1;
1460
+ };
1461
+ const checkAndFire = () => { if (isInCanvasViewport())
1462
+ fire(); };
1463
+ // Initial check — defer one animation frame so layout is settled after render.
1464
+ const rafId = requestAnimationFrame(checkAndFire);
1465
+ // Lenis changes scrollTop on .preview-scroll → native scroll event fires.
1466
+ // Builder also dispatches a synthetic scroll on .preview-lenis-content.
1467
+ // Listen on both so we catch all scroll notifications.
1468
+ const lenisEl = this.overlayService.getScrollContainer();
1469
+ canvasRoot.addEventListener('scroll', checkAndFire, { passive: true });
1470
+ if (lenisEl)
1471
+ lenisEl.addEventListener('scroll', checkAndFire, { passive: true });
1472
+ return () => {
1473
+ cancelAnimationFrame(rafId);
1474
+ canvasRoot.removeEventListener('scroll', checkAndFire);
1475
+ if (lenisEl)
1476
+ lenisEl.removeEventListener('scroll', checkAndFire);
1477
+ };
1478
+ }
1479
+ // Production mode: standard IntersectionObserver against browser viewport.
1480
+ const observer = new IntersectionObserver((entries) => {
1481
+ for (const entry of entries) {
1482
+ if (entry.isIntersecting) {
1483
+ fire();
1445
1484
  }
1446
1485
  }
1447
1486
  }, { threshold: 0.1 });
@@ -3848,6 +3887,9 @@ class BuilderComponent {
3848
3887
  // (Not previewScrollRef which is the Lenis eventsTarget — dispatching there
3849
3888
  // would cause Lenis to re-fire its scroll handler → infinite loop.)
3850
3889
  this.overlayService.setScrollContainer(this.lenisContentRef?.nativeElement || null);
3890
+ // .preview-scroll is the Lenis clipping container (overflow:hidden, scrollTop changes).
3891
+ // Used as IntersectionObserver root so viewportEnter triggers fire correctly on canvas scroll.
3892
+ this.overlayService.setIntersectionRoot(this.previewScrollRef?.nativeElement || null);
3851
3893
  // Register the root canvas as a dropzone.
3852
3894
  const canvasEl = document.getElementById('canvas-preview');
3853
3895
  if (canvasEl) {
@@ -3876,6 +3918,7 @@ class BuilderComponent {
3876
3918
  this.previewFrameRo?.disconnect();
3877
3919
  this.overlayService.setContainer(null);
3878
3920
  this.overlayService.setScrollContainer(null);
3921
+ this.overlayService.setIntersectionRoot(null);
3879
3922
  const canvasEl = document.getElementById('canvas-preview');
3880
3923
  if (canvasEl) {
3881
3924
  this.dragEngine.unregisterDropzone('canvas-preview', canvasEl);