nodebb-plugin-ezoic-infinite 1.6.64 → 1.6.66

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/client.js +23 -105
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.6.64",
3
+ "version": "1.6.66",
4
4
  "description": "Production-ready Ezoic infinite ads integration for NodeBB 4.x",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -79,17 +79,12 @@
79
79
  } catch (e) {}
80
80
  }
81
81
 
82
- // ─── CMP/TCF stability ──────────────────────────────────────────────────────
83
- function ensureTcfApiLocator() {
84
- try {
85
- if (typeof window.__tcfapi !== 'function' && typeof window.__cmp !== 'function') return;
86
- if (document.getElementById('__tcfapiLocator')) return;
87
- const f = document.createElement('iframe');
88
- f.style.display = 'none';
89
- f.id = f.name = '__tcfapiLocator';
90
- (document.body || document.documentElement).appendChild(f);
91
- } catch (e) {}
92
- }
82
+ // ensureTcfApiLocator intentionally removed:
83
+ // Creating our own __tcfapiLocator iframe conflicts with the CMP's own locator
84
+ // management and causes "Cannot read properties of null (reading 'postMessage')"
85
+ // errors when the CMP tries to postMessage to an iframe we created then navigated away.
86
+ // The CMP (Ezoic/IAB TCF) handles its own locator iframe lifecycle.
87
+ function ensureTcfApiLocator() { /* no-op */ }
93
88
 
94
89
  // ─── Ezoic min-height tightener ─────────────────────────────────────────────
95
90
  // Ezoic injects `min-height:400px !important` on nested wrappers via inline
@@ -402,22 +397,12 @@
402
397
 
403
398
  // ─── Insertion primitives ───────────────────────────────────────────────────
404
399
 
405
- /** Extract the stable DOM id (tid/pid/cid) from an anchor element. */
406
- function getAnchorStableId(el) {
407
- if (!el) return '';
408
- return el.getAttribute('data-tid') || el.getAttribute('data-pid') ||
409
- el.getAttribute('data-cid') || el.getAttribute('data-index') || '';
410
- }
411
-
412
- function buildWrap(id, kindClass, afterPos, createPlaceholder, anchorStableId) {
400
+ function buildWrap(id, kindClass, afterPos, createPlaceholder) {
413
401
  const wrap = document.createElement('div');
414
402
  wrap.className = WRAP_CLASS + ' ' + kindClass;
415
403
  wrap.setAttribute('data-ezoic-after', String(afterPos));
416
404
  wrap.setAttribute('data-ezoic-wrapid', String(id));
417
405
  wrap.setAttribute('data-created', String(now()));
418
- // Store the anchor element's stable DOM id so we can re-anchor after
419
- // NodeBB re-renders the list (which changes ordinal positions).
420
- if (anchorStableId) wrap.setAttribute('data-ezoic-anchor', String(anchorStableId));
421
406
  if (afterPos === 1) wrap.setAttribute('data-ezoic-pin', '1');
422
407
  wrap.style.width = '100%';
423
408
  if (createPlaceholder) {
@@ -429,27 +414,14 @@
429
414
  return wrap;
430
415
  }
431
416
 
432
- /**
433
- * Find a wrap that is already anchored to a specific stable id (tid/pid/cid).
434
- * Used to prevent double-injection when NodeBB re-renders and ordinals shift.
435
- */
436
- function findWrapByAnchor(kindClass, anchorStableId) {
437
- if (!anchorStableId) return null;
438
- return document.querySelector(
439
- '.' + WRAP_CLASS + '.' + kindClass + '[data-ezoic-anchor="' + CSS.escape(String(anchorStableId)) + '"]'
440
- );
441
- }
442
-
443
417
  function insertAfter(target, id, kindClass, afterPos) {
444
418
  if (!target || !target.insertAdjacentElement) return null;
445
419
  if (findWrap(kindClass, afterPos)) return null;
446
- const anchorId = getAnchorStableId(target);
447
- if (anchorId && findWrapByAnchor(kindClass, anchorId)) return null; // already anchored
448
420
  if (insertingIds.has(id)) return null;
449
421
  const existingPh = document.getElementById(PLACEHOLDER_PREFIX + id);
450
422
  insertingIds.add(id);
451
423
  try {
452
- const wrap = buildWrap(id, kindClass, afterPos, !existingPh, anchorId);
424
+ const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
453
425
  target.insertAdjacentElement('afterend', wrap);
454
426
  if (existingPh) {
455
427
  existingPh.setAttribute('data-ezoic-id', String(id));
@@ -494,21 +466,12 @@
494
466
  const itemSet = new Set(items);
495
467
  const isMessage = kindClass === 'ezoic-ad-message';
496
468
  // Between/category ads are NEVER fully released — only hidden/shown.
469
+ // Releasing frees IDs into the pool → re-injection at wrong positions.
497
470
  const allowRelease = isMessage;
498
471
  let removed = 0;
499
472
 
500
- // Build a set of stable anchor ids currently in the DOM (tid/pid/cid).
501
- const liveAnchorIds = new Set();
502
- items.forEach((el) => {
503
- const sid = getAnchorStableId(el);
504
- if (sid) liveAnchorIds.add(sid);
505
- });
506
-
507
- const anchorIsLive = (wrap) => {
508
- // Primary: check by stable id (survives NodeBB re-renders).
509
- const sid = wrap.getAttribute('data-ezoic-anchor');
510
- if (sid) return liveAnchorIds.has(sid);
511
- // Fallback: DOM-proximity check (for wraps without stable id).
473
+ const hasNearbyItem = (wrap) => {
474
+ // If wrap is inside a li.nodebb-ezoic-host, check the HOST's siblings.
512
475
  const pivot = (wrap.parentElement && wrap.parentElement.classList &&
513
476
  wrap.parentElement.classList.contains(HOST_CLASS))
514
477
  ? wrap.parentElement : wrap;
@@ -530,12 +493,12 @@
530
493
  const created = parseInt(wrap.getAttribute('data-created') || '0', 10);
531
494
  if (created && (now() - created) < keepEmptyWrapMs()) return;
532
495
 
533
- if (anchorIsLive(wrap)) {
496
+ if (hasNearbyItem(wrap)) {
534
497
  try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
535
498
  return;
536
499
  }
537
500
 
538
- // Anchor not in DOM → hide.
501
+ // Anchor gone → hide.
539
502
  try { wrap.classList.add('ez-orphan-hidden'); wrap.style.display = 'none'; } catch (e) {}
540
503
  if (!allowRelease) return;
541
504
 
@@ -552,55 +515,6 @@
552
515
  return removed;
553
516
  }
554
517
 
555
- /**
556
- * Re-anchor between/category wraps after a NodeBB list re-render.
557
- *
558
- * When NodeBB rebuilds the topic list (infinite scroll above, sort change…),
559
- * it re-inserts topic <li> elements in a new DOM order. Our wraps are still
560
- * in the DOM but they may now sit between wrong topics because their ordinals
561
- * changed. We use the stable anchor id (data-tid / data-pid) recorded at
562
- * insertion time to find each wrap's correct anchor topic and move it there.
563
- */
564
- function reanchorWraps(kindClass, items) {
565
- // Build tid → element map from the current live items.
566
- const tidMap = new Map();
567
- items.forEach((el) => {
568
- const sid = getAnchorStableId(el);
569
- if (sid) tidMap.set(sid, el);
570
- });
571
-
572
- document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass).forEach((wrap) => {
573
- try {
574
- const sid = wrap.getAttribute('data-ezoic-anchor');
575
- if (!sid) return;
576
- const anchor = tidMap.get(sid);
577
- if (!anchor || !anchor.isConnected) return;
578
-
579
- // Check if the wrap is already correctly positioned (immediately after anchor).
580
- // Account for li.nodebb-ezoic-host wrapper.
581
- const wrapOrHost = (wrap.parentElement && wrap.parentElement.classList &&
582
- wrap.parentElement.classList.contains(HOST_CLASS))
583
- ? wrap.parentElement : wrap;
584
-
585
- if (wrapOrHost.previousElementSibling === anchor) {
586
- // Already in the right place — just un-hide if it was hidden.
587
- try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
588
- return;
589
- }
590
-
591
- // Update data-ezoic-after to the anchor's current ordinal so findWrap still works.
592
- const newOrdinal = getAnchorStableId(anchor)
593
- ? (parseInt(anchor.getAttribute('data-index') || '-1', 10) + 1) || 0
594
- : 0;
595
- if (newOrdinal > 0) wrap.setAttribute('data-ezoic-after', String(newOrdinal));
596
-
597
- // Move the wrap (or its host) to after the anchor.
598
- anchor.insertAdjacentElement('afterend', wrapOrHost);
599
- try { wrap.classList.remove('ez-orphan-hidden'); wrap.style.display = ''; } catch (e) {}
600
- } catch (e) {}
601
- });
602
- }
603
-
604
518
  function decluster(kindClass) {
605
519
  const wraps = Array.from(document.querySelectorAll('.' + WRAP_CLASS + '.' + kindClass));
606
520
  if (wraps.length < 2) return 0;
@@ -994,6 +908,16 @@
994
908
  if (inserted >= maxInserts) break;
995
909
  const el = ordinalMap.get(afterPos);
996
910
  if (!el || !el.isConnected) continue;
911
+
912
+ // Never inject after an element that is fully above the viewport top.
913
+ // This is the definitive guard: NodeBB loads items above the fold when the
914
+ // user scrolls up. Those items have rect.bottom < 0 at the moment of load.
915
+ // Any negative margin ("a little above is ok") still causes the pile-up
916
+ // because NodeBB loads a whole batch just above — we must use exactly 0.
917
+ try {
918
+ if (el.getBoundingClientRect().bottom < 0) continue;
919
+ } catch (e) {}
920
+
997
921
  if (isAdjacentAd(el)) continue;
998
922
  if (findWrap(kindClass, afterPos)) continue;
999
923
 
@@ -1052,11 +976,6 @@
1052
976
 
1053
977
  } else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
1054
978
  const items = getTopicItems();
1055
- // Re-anchor FIRST: move existing wraps to their correct topic after any
1056
- // NodeBB list re-render (ordinals may have shifted). This must happen before
1057
- // pruneOrphanWraps (which uses live anchor ids) and before injectBetween
1058
- // (which checks findWrap/findWrapByAnchor to avoid duplicates).
1059
- reanchorWraps('ezoic-ad-between', items);
1060
979
  pruneOrphanWraps('ezoic-ad-between', items);
1061
980
  if (canInject) {
1062
981
  inserted += injectBetween('ezoic-ad-between', items,
@@ -1068,7 +987,6 @@
1068
987
 
1069
988
  } else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
1070
989
  const items = getCategoryItems();
1071
- reanchorWraps('ezoic-ad-categories', items);
1072
990
  pruneOrphanWraps('ezoic-ad-categories', items);
1073
991
  if (canInject) {
1074
992
  inserted += injectBetween('ezoic-ad-categories', items,