nodebb-plugin-ezoic-infinite 1.5.84 → 1.5.86

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 +38 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.84",
3
+ "version": "1.5.86",
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
@@ -486,6 +486,24 @@ function globalGapFixInit() {
486
486
  return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-after="${afterPos}"]`);
487
487
  }
488
488
 
489
+ function getStableAnchorKey(el) {
490
+ try {
491
+ if (!el) return '';
492
+ const pid = el.getAttribute('data-pid');
493
+ if (pid) return 'pid:' + pid;
494
+ const tid = el.getAttribute('data-tid');
495
+ if (tid) return 'tid:' + tid;
496
+ const slug = el.getAttribute('data-slug');
497
+ if (slug) return 'slug:' + slug;
498
+ } catch (e) {}
499
+ return '';
500
+ }
501
+
502
+ function findWrapByAnchor(kindClass, anchorKey) {
503
+ if (!anchorKey) return null;
504
+ return document.querySelector(`.${WRAP_CLASS}.${kindClass}[data-ezoic-anchor="${anchorKey}"]`);
505
+ }
506
+
489
507
  // ---------------- placeholder pool ----------------
490
508
 
491
509
  function getPoolEl() {
@@ -636,12 +654,13 @@ function globalGapFixInit() {
636
654
 
637
655
  // ---------------- insertion primitives ----------------
638
656
 
639
- function buildWrap(id, kindClass, afterPos, createPlaceholder) {
657
+ function buildWrap(id, kindClass, afterPos, createPlaceholder, anchorKey) {
640
658
  const wrap = document.createElement('div');
641
659
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
642
660
  wrap.setAttribute('data-ezoic-after', String(afterPos));
643
661
  wrap.setAttribute('data-ezoic-wrapid', String(id));
644
662
  wrap.setAttribute('data-created', String(now()));
663
+ if (anchorKey) wrap.setAttribute('data-ezoic-anchor', String(anchorKey));
645
664
  // "Pinned" placements (after the first item) should remain stable.
646
665
  if (afterPos === 1) {
647
666
  wrap.setAttribute('data-ezoic-pin', '1');
@@ -658,16 +677,16 @@ function globalGapFixInit() {
658
677
  return wrap;
659
678
  }
660
679
 
661
- function insertAfter(target, id, kindClass, afterPos) {
680
+ function insertAfter(target, id, kindClass, afterPos, anchorKey) {
662
681
  if (!target || !target.insertAdjacentElement) return null;
663
- if (findWrap(kindClass, afterPos)) return null;
682
+ if (findWrap(kindClass, afterPos) || findWrapByAnchor(kindClass, anchorKey)) return null;
664
683
  if (insertingIds.has(id)) return null;
665
684
 
666
685
  const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
667
686
 
668
687
  insertingIds.add(id);
669
688
  try {
670
- const wrap = buildWrap(id, kindClass, afterPos, !existingPh);
689
+ const wrap = buildWrap(id, kindClass, afterPos, !existingPh, anchorKey);
671
690
  target.insertAdjacentElement('afterend', wrap);
672
691
 
673
692
  // If placeholder exists elsewhere (including pool), move it into the wrapper.
@@ -1092,6 +1111,16 @@ function buildOrdinalMap(items) {
1092
1111
  if (!el) continue;
1093
1112
  if (!el || !el.isConnected) continue;
1094
1113
  if (isAdjacentAd(el)) continue;
1114
+ const anchorKey = getStableAnchorKey(el);
1115
+ const existingByAnchor = findWrapByAnchor(kindClass, anchorKey);
1116
+ if (existingByAnchor && existingByAnchor.isConnected) {
1117
+ const next = el.nextElementSibling;
1118
+ if (next !== existingByAnchor) {
1119
+ try { el.insertAdjacentElement('afterend', existingByAnchor); } catch (e) {}
1120
+ }
1121
+ existingByAnchor.setAttribute('data-ezoic-after', String(afterPos));
1122
+ continue;
1123
+ }
1095
1124
  if (findWrap(kindClass, afterPos)) continue;
1096
1125
 
1097
1126
  let id = pickIdFromAll(allIds, cursorKey);
@@ -1104,7 +1133,7 @@ function buildOrdinalMap(items) {
1104
1133
  if (!id) {
1105
1134
  // Safe mode: disable recycling for topic message ads to prevent visual "jumping"
1106
1135
  // (ads seemingly moving back under the first post during virtualized scroll).
1107
- const allowRecycle = kindClass !== 'ezoic-ad-message';
1136
+ const allowRecycle = false; // hard-disable recycling to prevent wrappers jumping to top on virtualized/infinite scroll
1108
1137
  recycledWrap = (allowRecycle && scrollDir > 0) ? pickRecyclableWrap(kindClass) : null;
1109
1138
  if (recycledWrap) {
1110
1139
  id = recycledWrap.getAttribute('data-ezoic-wrapid') || '';
@@ -1115,7 +1144,7 @@ function buildOrdinalMap(items) {
1115
1144
 
1116
1145
  const wrap = recycledWrap
1117
1146
  ? moveWrapAfter(el, recycledWrap, kindClass, afterPos)
1118
- : insertAfter(el, id, kindClass, afterPos);
1147
+ : insertAfter(el, id, kindClass, afterPos, anchorKey);
1119
1148
  if (!wrap) continue;
1120
1149
 
1121
1150
  // observePlaceholder is safe for existing ids (it is idempotent) and helps with "late fill"
@@ -1195,7 +1224,7 @@ function buildOrdinalMap(items) {
1195
1224
  state.allPosts,
1196
1225
  'curPosts'
1197
1226
  );
1198
- decluster('ezoic-ad-message');
1227
+ // decluster disabled for stability on virtualized lists
1199
1228
  }
1200
1229
  } else if (kind === 'categoryTopics') {
1201
1230
  if (normalizeBool(cfg.enableBetweenAds)) {
@@ -1209,7 +1238,7 @@ function buildOrdinalMap(items) {
1209
1238
  state.allTopics,
1210
1239
  'curTopics'
1211
1240
  );
1212
- decluster('ezoic-ad-between');
1241
+ // decluster disabled for stability on virtualized lists
1213
1242
  }
1214
1243
  } else if (kind === 'categories') {
1215
1244
  if (normalizeBool(cfg.enableCategoryAds)) {
@@ -1223,7 +1252,7 @@ function buildOrdinalMap(items) {
1223
1252
  state.allCategories,
1224
1253
  'curCategories'
1225
1254
  );
1226
- decluster('ezoic-ad-categories');
1255
+ // decluster disabled for stability on virtualized lists
1227
1256
  }
1228
1257
  }
1229
1258