nodebb-plugin-ezoic-infinite 1.5.63 → 1.5.65

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.63",
3
+ "version": "1.5.65",
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
@@ -99,6 +99,16 @@
99
99
 
100
100
  const insertingIds = new Set();
101
101
 
102
+ function unemptyIfFilled(wrap, ph) {
103
+ try {
104
+ if (!wrap || !ph) return false;
105
+ const hasAd = !!(ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
106
+ if (hasAd) wrap.classList.remove('is-empty');
107
+ return hasAd;
108
+ } catch (e) { return false; }
109
+ }
110
+
111
+
102
112
 
103
113
  function markEmptyWrapper(id) {
104
114
  try {
@@ -116,6 +126,17 @@
116
126
  // consider empty if only whitespace and no iframes/ins/img
117
127
  const hasAd = !!(ph2.querySelector && ph2.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
118
128
  if (!hasAd) w2.classList.add('is-empty');
129
+ // If the ad fills later, immediately uncollapse to avoid "missing ads" perception.
130
+ if (!unemptyIfFilled(w2, ph2)) {
131
+ try {
132
+ const mo = new MutationObserver(() => {
133
+ if (unemptyIfFilled(w2, ph2)) { try { mo.disconnect(); } catch (e) {} }
134
+ });
135
+ mo.observe(ph2, { childList: true, subtree: true });
136
+ // safety stop
137
+ setTimeout(() => { try { mo.disconnect(); } catch (e) {} }, 30000);
138
+ } catch (e) {}
139
+ }
119
140
  } catch (e) {}
120
141
  }, 3500);
121
142
  } catch (e) {}
@@ -234,22 +255,40 @@
234
255
  window.__nodebbEzoicPatched = true;
235
256
  const orig = ez.showAds;
236
257
 
258
+ // Important: preserve the original calling convention.
259
+ // Some Ezoic builds expect an array; calling one-by-one can lead to
260
+ // repeated define attempts and "Placeholder Id ... already been defined".
237
261
  ez.showAds = function (...args) {
238
262
  if (isBlocked()) return;
239
263
 
264
+ const now = Date.now();
240
265
  let ids = [];
241
- if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
266
+ const isArrayCall = (args.length === 1 && Array.isArray(args[0]));
267
+ if (isArrayCall) ids = args[0];
242
268
  else ids = args;
243
269
 
270
+ const filtered = [];
244
271
  const seen = new Set();
245
272
  for (const v of ids) {
246
273
  const id = parseInt(v, 10);
247
274
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
248
275
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
249
276
  if (!ph || !ph.isConnected) continue;
277
+
278
+ // Extra throttle to avoid rapid duplicate defines during ajaxify churn
279
+ const last = state.lastShowById.get(id) || 0;
280
+ if (now - last < 650) continue;
281
+ state.lastShowById.set(id, now);
282
+
250
283
  seen.add(id);
251
- try { orig.call(ez, id); } catch (e) {}
284
+ filtered.push(id);
252
285
  }
286
+
287
+ if (!filtered.length) return;
288
+ try {
289
+ if (isArrayCall) orig.call(ez, filtered);
290
+ else orig.apply(ez, filtered);
291
+ } catch (e) {}
253
292
  };
254
293
  } catch (e) {}
255
294
  };
@@ -347,11 +386,21 @@ function withInternalDomChange(fn) {
347
386
  // NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
348
387
  let ok = false;
349
388
  let prev = wrap.previousElementSibling;
350
- for (let i = 0; i < 3 && prev; i++) {
389
+ for (let i = 0; i < 8 && prev; i++) {
351
390
  if (itemSet.has(prev)) { ok = true; break; }
352
391
  prev = prev.previousElementSibling;
353
392
  }
354
393
 
394
+ // If it is already filled (iframe/ins/img), be conservative and keep it.
395
+ // Prevents ads "disappearing too fast" during ajaxify churn / minor rerenders.
396
+ if (!ok) {
397
+ try {
398
+ const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
399
+ const filled = !!(ph && ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
400
+ if (filled) ok = true;
401
+ } catch (e) {}
402
+ }
403
+
355
404
  if (!ok) {
356
405
  const id = getWrapIdFromWrap(wrap);
357
406
  withInternalDomChange(() => {
@@ -368,6 +417,28 @@ function withInternalDomChange(fn) {
368
417
  return removed;
369
418
  }
370
419
 
420
+ function declusterWraps(kindClass) {
421
+ try {
422
+ const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
423
+ if (wraps.length < 2) return;
424
+ for (let i = 1; i < wraps.length; i++) {
425
+ const w = wraps[i];
426
+ if (!w || !w.isConnected) continue;
427
+ // If previous siblings contain another wrap within 2 hops, remove this one.
428
+ let prev = w.previousElementSibling;
429
+ let hops = 0;
430
+ while (prev && hops < 3) {
431
+ if (prev.classList && prev.classList.contains(WRAP_CLASS)) {
432
+ withInternalDomChange(() => { try { w.remove(); } catch (e) {} });
433
+ break;
434
+ }
435
+ prev = prev.previousElementSibling;
436
+ hops++;
437
+ }
438
+ }
439
+ } catch (e) {}
440
+ }
441
+
371
442
  function refreshEmptyState(id) {
372
443
  // After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
373
444
  window.setTimeout(() => {
@@ -383,17 +454,25 @@ function withInternalDomChange(fn) {
383
454
  }, 3500);
384
455
  }
385
456
 
386
- function buildWrap(id, kindClass, afterPos) {
457
+ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
387
458
  const wrap = document.createElement('div');
388
459
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
389
460
  wrap.setAttribute('data-ezoic-after', String(afterPos));
390
461
  wrap.setAttribute('data-ezoic-wrapid', String(id));
391
462
  wrap.style.width = '100%';
392
463
 
393
- const ph = document.createElement('div');
394
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
395
- ph.setAttribute('data-ezoic-id', String(id));
396
- wrap.appendChild(ph);
464
+ if (existingPlaceholder && existingPlaceholder.nodeType === 1) {
465
+ try {
466
+ existingPlaceholder.id = `${PLACEHOLDER_PREFIX}${id}`;
467
+ existingPlaceholder.setAttribute('data-ezoic-id', String(id));
468
+ } catch (e) {}
469
+ wrap.appendChild(existingPlaceholder);
470
+ } else {
471
+ const ph = document.createElement('div');
472
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
473
+ ph.setAttribute('data-ezoic-id', String(id));
474
+ wrap.appendChild(ph);
475
+ }
397
476
 
398
477
  return wrap;
399
478
  }
@@ -407,24 +486,28 @@ function buildWrap(id, kindClass, afterPos) {
407
486
  if (findWrap(kindClass, afterPos)) return null;
408
487
  if (insertingIds.has(id)) return null;
409
488
 
410
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
411
-
412
489
  insertingIds.add(id);
413
490
  try {
414
- const wrap = buildWrap(id, kindClass, afterPos);
415
- target.insertAdjacentElement('afterend', wrap);
416
-
417
- // If a placeholder with this id already exists elsewhere (some Ezoic flows
418
- // pre-create placeholders), move it into our wrapper instead of aborting.
419
- // replaceChild moves the node atomically (no detach window).
420
- if (existingPh && existingPh !== wrap.firstElementChild) {
491
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
492
+
493
+ // CRITICAL: never create a second element with the same id, even briefly.
494
+ // That can trigger "Placeholder Id ... already been defined" during load.
495
+ // If an existing placeholder already exists, move it into the new wrapper
496
+ // before inserting the wrapper into the DOM.
497
+ let moved = null;
498
+ if (existingPh && existingPh.isConnected) {
499
+ moved = existingPh;
500
+ // If it was inside one of our wrappers, drop that empty wrapper.
421
501
  try {
422
- existingPh.setAttribute('data-ezoic-id', String(id));
423
- wrap.replaceChild(existingPh, wrap.firstElementChild);
424
- } catch (e) {
425
- // Keep the new placeholder if replace fails.
426
- }
502
+ const oldWrap = moved.closest && moved.closest(`.${WRAP_CLASS}`);
503
+ if (oldWrap && oldWrap.parentNode) {
504
+ withInternalDomChange(() => { try { oldWrap.remove(); } catch (e) {} });
505
+ }
506
+ } catch (e) {}
427
507
  }
508
+
509
+ const wrap = buildWrap(id, kindClass, afterPos, moved);
510
+ target.insertAdjacentElement('afterend', wrap);
428
511
  return wrap;
429
512
  } finally {
430
513
  insertingIds.delete(id);
@@ -752,6 +835,7 @@ function startShow(id) {
752
835
  state.allPosts,
753
836
  'curPosts'
754
837
  );
838
+ declusterWraps('ezoic-ad-message');
755
839
  }
756
840
  } else if (kind === 'categoryTopics') {
757
841
  if (normalizeBool(cfg.enableBetweenAds)) {
@@ -765,6 +849,7 @@ function startShow(id) {
765
849
  state.allTopics,
766
850
  'curTopics'
767
851
  );
852
+ declusterWraps('ezoic-ad-between');
768
853
  }
769
854
  } else if (kind === 'categories') {
770
855
  if (normalizeBool(cfg.enableCategoryAds)) {
@@ -778,6 +863,7 @@ function startShow(id) {
778
863
  state.allCategories,
779
864
  'curCategories'
780
865
  );
866
+ declusterWraps('ezoic-ad-categories');
781
867
  }
782
868
  }
783
869
  }
package/public/style.css CHANGED
@@ -29,17 +29,17 @@
29
29
  display: block !important;
30
30
  margin: 0 !important;
31
31
  padding: 0 !important;
32
- height: 0 !important;
33
- min-height: 0 !important;
32
+ height: 1px !important;
33
+ min-height: 1px !important;
34
34
  overflow: hidden !important;
35
35
  }
36
36
 
37
37
  .nodebb-ezoic-wrap {
38
- min-height: 0 !important;
38
+ min-height: 1px !important;
39
39
  }
40
40
 
41
41
  .nodebb-ezoic-wrap > [id^="ezoic-pub-ad-placeholder-"] {
42
- min-height: 0 !important;
42
+ min-height: 1px !important;
43
43
  }
44
44
 
45
45
  /*