nodebb-plugin-ezoic-infinite 1.5.62 → 1.5.64

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 +94 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.5.62",
3
+ "version": "1.5.64",
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
@@ -234,22 +234,40 @@
234
234
  window.__nodebbEzoicPatched = true;
235
235
  const orig = ez.showAds;
236
236
 
237
+ // Important: preserve the original calling convention.
238
+ // Some Ezoic builds expect an array; calling one-by-one can lead to
239
+ // repeated define attempts and "Placeholder Id ... already been defined".
237
240
  ez.showAds = function (...args) {
238
241
  if (isBlocked()) return;
239
242
 
243
+ const now = Date.now();
240
244
  let ids = [];
241
- if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
245
+ const isArrayCall = (args.length === 1 && Array.isArray(args[0]));
246
+ if (isArrayCall) ids = args[0];
242
247
  else ids = args;
243
248
 
249
+ const filtered = [];
244
250
  const seen = new Set();
245
251
  for (const v of ids) {
246
252
  const id = parseInt(v, 10);
247
253
  if (!Number.isFinite(id) || id <= 0 || seen.has(id)) continue;
248
254
  const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
249
255
  if (!ph || !ph.isConnected) continue;
256
+
257
+ // Extra throttle to avoid rapid duplicate defines during ajaxify churn
258
+ const last = state.lastShowById.get(id) || 0;
259
+ if (now - last < 650) continue;
260
+ state.lastShowById.set(id, now);
261
+
250
262
  seen.add(id);
251
- try { orig.call(ez, id); } catch (e) {}
263
+ filtered.push(id);
252
264
  }
265
+
266
+ if (!filtered.length) return;
267
+ try {
268
+ if (isArrayCall) orig.call(ez, filtered);
269
+ else orig.apply(ez, filtered);
270
+ } catch (e) {}
253
271
  };
254
272
  } catch (e) {}
255
273
  };
@@ -329,12 +347,12 @@ function withInternalDomChange(fn) {
329
347
  }
330
348
 
331
349
  function safeDestroyById(id) {
332
- try {
333
- const ez = window.ezstandalone;
334
- if (ez && typeof ez.destroyPlaceholders === 'function') {
335
- ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]);
336
- }
337
- } catch (e) {}
350
+ // IMPORTANT:
351
+ // Do NOT call ez.destroyPlaceholders here.
352
+ // In NodeBB ajaxify/infinite-scroll flows, Ezoic can be mid-refresh.
353
+ // Destroy calls can create churn, reduce fill, and generate "does not exist" spam.
354
+ // We only remove our wrapper; Ezoic manages slot lifecycle.
355
+ return;
338
356
  }
339
357
 
340
358
  function pruneOrphanWraps(kindClass, items) {
@@ -347,11 +365,21 @@ function withInternalDomChange(fn) {
347
365
  // NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
348
366
  let ok = false;
349
367
  let prev = wrap.previousElementSibling;
350
- for (let i = 0; i < 3 && prev; i++) {
368
+ for (let i = 0; i < 8 && prev; i++) {
351
369
  if (itemSet.has(prev)) { ok = true; break; }
352
370
  prev = prev.previousElementSibling;
353
371
  }
354
372
 
373
+ // If it is already filled (iframe/ins/img), be conservative and keep it.
374
+ // Prevents ads "disappearing too fast" during ajaxify churn / minor rerenders.
375
+ if (!ok) {
376
+ try {
377
+ const ph = wrap.querySelector && wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
378
+ const filled = !!(ph && ph.querySelector && ph.querySelector('iframe, ins, img, .ez-ad, .ezoic-ad'));
379
+ if (filled) ok = true;
380
+ } catch (e) {}
381
+ }
382
+
355
383
  if (!ok) {
356
384
  const id = getWrapIdFromWrap(wrap);
357
385
  withInternalDomChange(() => {
@@ -368,6 +396,28 @@ function withInternalDomChange(fn) {
368
396
  return removed;
369
397
  }
370
398
 
399
+ function declusterWraps(kindClass) {
400
+ try {
401
+ const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
402
+ if (wraps.length < 2) return;
403
+ for (let i = 1; i < wraps.length; i++) {
404
+ const w = wraps[i];
405
+ if (!w || !w.isConnected) continue;
406
+ // If previous siblings contain another wrap within 2 hops, remove this one.
407
+ let prev = w.previousElementSibling;
408
+ let hops = 0;
409
+ while (prev && hops < 3) {
410
+ if (prev.classList && prev.classList.contains(WRAP_CLASS)) {
411
+ withInternalDomChange(() => { try { w.remove(); } catch (e) {} });
412
+ break;
413
+ }
414
+ prev = prev.previousElementSibling;
415
+ hops++;
416
+ }
417
+ }
418
+ } catch (e) {}
419
+ }
420
+
371
421
  function refreshEmptyState(id) {
372
422
  // After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
373
423
  window.setTimeout(() => {
@@ -383,17 +433,25 @@ function withInternalDomChange(fn) {
383
433
  }, 3500);
384
434
  }
385
435
 
386
- function buildWrap(id, kindClass, afterPos) {
436
+ function buildWrap(id, kindClass, afterPos, existingPlaceholder) {
387
437
  const wrap = document.createElement('div');
388
438
  wrap.className = `${WRAP_CLASS} ${kindClass}`;
389
439
  wrap.setAttribute('data-ezoic-after', String(afterPos));
390
440
  wrap.setAttribute('data-ezoic-wrapid', String(id));
391
441
  wrap.style.width = '100%';
392
442
 
393
- const ph = document.createElement('div');
394
- ph.id = `${PLACEHOLDER_PREFIX}${id}`;
395
- ph.setAttribute('data-ezoic-id', String(id));
396
- wrap.appendChild(ph);
443
+ if (existingPlaceholder && existingPlaceholder.nodeType === 1) {
444
+ try {
445
+ existingPlaceholder.id = `${PLACEHOLDER_PREFIX}${id}`;
446
+ existingPlaceholder.setAttribute('data-ezoic-id', String(id));
447
+ } catch (e) {}
448
+ wrap.appendChild(existingPlaceholder);
449
+ } else {
450
+ const ph = document.createElement('div');
451
+ ph.id = `${PLACEHOLDER_PREFIX}${id}`;
452
+ ph.setAttribute('data-ezoic-id', String(id));
453
+ wrap.appendChild(ph);
454
+ }
397
455
 
398
456
  return wrap;
399
457
  }
@@ -407,12 +465,27 @@ function buildWrap(id, kindClass, afterPos) {
407
465
  if (findWrap(kindClass, afterPos)) return null;
408
466
  if (insertingIds.has(id)) return null;
409
467
 
410
- const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
411
- if (existingPh && existingPh.isConnected) return null;
412
-
413
468
  insertingIds.add(id);
414
469
  try {
415
- const wrap = buildWrap(id, kindClass, afterPos);
470
+ const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
471
+
472
+ // CRITICAL: never create a second element with the same id, even briefly.
473
+ // That can trigger "Placeholder Id ... already been defined" during load.
474
+ // If an existing placeholder already exists, move it into the new wrapper
475
+ // before inserting the wrapper into the DOM.
476
+ let moved = null;
477
+ if (existingPh && existingPh.isConnected) {
478
+ moved = existingPh;
479
+ // If it was inside one of our wrappers, drop that empty wrapper.
480
+ try {
481
+ const oldWrap = moved.closest && moved.closest(`.${WRAP_CLASS}`);
482
+ if (oldWrap && oldWrap.parentNode) {
483
+ withInternalDomChange(() => { try { oldWrap.remove(); } catch (e) {} });
484
+ }
485
+ } catch (e) {}
486
+ }
487
+
488
+ const wrap = buildWrap(id, kindClass, afterPos, moved);
416
489
  target.insertAdjacentElement('afterend', wrap);
417
490
  return wrap;
418
491
  } finally {
@@ -741,6 +814,7 @@ function startShow(id) {
741
814
  state.allPosts,
742
815
  'curPosts'
743
816
  );
817
+ declusterWraps('ezoic-ad-message');
744
818
  }
745
819
  } else if (kind === 'categoryTopics') {
746
820
  if (normalizeBool(cfg.enableBetweenAds)) {
@@ -754,6 +828,7 @@ function startShow(id) {
754
828
  state.allTopics,
755
829
  'curTopics'
756
830
  );
831
+ declusterWraps('ezoic-ad-between');
757
832
  }
758
833
  } else if (kind === 'categories') {
759
834
  if (normalizeBool(cfg.enableCategoryAds)) {
@@ -767,6 +842,7 @@ function startShow(id) {
767
842
  state.allCategories,
768
843
  'curCategories'
769
844
  );
845
+ declusterWraps('ezoic-ad-categories');
770
846
  }
771
847
  }
772
848
  }