nodebb-plugin-ezoic-infinite 1.5.28 → 1.5.30
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 +1 -1
- package/public/client.js +242 -235
- package/public/style.css +2 -1
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -13,27 +13,23 @@
|
|
|
13
13
|
// Preload before viewport (tune if you want even earlier)
|
|
14
14
|
const PRELOAD_ROOT_MARGIN = '1200px 0px';
|
|
15
15
|
|
|
16
|
-
// Windowing: keep ads mostly around the viewport to avoid DOM bloat + ID saturation
|
|
17
|
-
const WINDOW_MIN_ITEMS = 24; // minimum scan window
|
|
18
|
-
const WINDOW_MAX_ITEMS = 120; // maximum scan window
|
|
19
|
-
const WINDOW_BUFFER_PX = 900; // how far outside viewport counts as "near"
|
|
20
|
-
const PURGE_BUFFER_ITEMS = 12; // extra items outside the window where we keep wrappers
|
|
21
|
-
const RECYCLE_COOLDOWN_MS = 1500; // anti-loop when at saturation / heavy churn
|
|
22
|
-
|
|
23
|
-
// Soft-block during navigation or heavy DOM churn (avoid "placeholder does not exist" spam)
|
|
24
|
-
let EZOIC_BLOCKED_UNTIL = 0;
|
|
25
|
-
|
|
26
16
|
const SELECTORS = {
|
|
27
17
|
topicItem: 'li[component="category/topic"]',
|
|
28
18
|
postItem: '[component="post"][data-pid]',
|
|
29
19
|
categoryItem: 'li[component="categories/category"]',
|
|
30
20
|
};
|
|
31
21
|
|
|
22
|
+
// Soft block during navigation / heavy DOM churn to avoid “placeholder does not exist” spam
|
|
23
|
+
let blockedUntil = 0;
|
|
24
|
+
function isBlocked() {
|
|
25
|
+
return Date.now() < blockedUntil;
|
|
26
|
+
}
|
|
27
|
+
|
|
32
28
|
const state = {
|
|
33
29
|
pageKey: null,
|
|
34
30
|
cfg: null,
|
|
35
31
|
|
|
36
|
-
// Full
|
|
32
|
+
// Full lists (never consumed) + cursors for round-robin reuse
|
|
37
33
|
allTopics: [],
|
|
38
34
|
allPosts: [],
|
|
39
35
|
allCategories: [],
|
|
@@ -43,6 +39,11 @@
|
|
|
43
39
|
|
|
44
40
|
// throttle per placeholder id
|
|
45
41
|
lastShowById: new Map(),
|
|
42
|
+
internalDomChange: 0,
|
|
43
|
+
lastRecycleAt: { topic: 0, categoryTopics: 0, categories: 0 },
|
|
44
|
+
|
|
45
|
+
// track placeholders that have been shown at least once in this pageview
|
|
46
|
+
usedOnce: new Set(),
|
|
46
47
|
|
|
47
48
|
// observers / schedulers
|
|
48
49
|
domObs: null,
|
|
@@ -51,28 +52,22 @@
|
|
|
51
52
|
|
|
52
53
|
// hero
|
|
53
54
|
heroDoneForPage: false,
|
|
54
|
-
|
|
55
|
-
// internal DOM changes (to ignore our own mutations)
|
|
56
|
-
internalDomChange: 0,
|
|
57
|
-
|
|
58
|
-
// recycle cooldown per kind
|
|
59
|
-
lastRecycleAt: {
|
|
60
|
-
topic: 0,
|
|
61
|
-
categoryTopics: 0,
|
|
62
|
-
categories: 0,
|
|
63
|
-
},
|
|
64
55
|
};
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
const insertingIds = new Set();
|
|
58
|
+
|
|
59
|
+
// Debug logs (enable with localStorage.ezoicInfiniteDebug = "1")
|
|
60
|
+
function dbg(...args) {
|
|
61
|
+
try {
|
|
62
|
+
if (window && window.localStorage && window.localStorage.getItem('ezoicInfiniteDebug') === '1') {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.log('[ezoicInfinite]', ...args);
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {}
|
|
73
67
|
}
|
|
74
68
|
|
|
75
69
|
// ---------- small utils ----------
|
|
70
|
+
|
|
76
71
|
function normalizeBool(v) {
|
|
77
72
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
78
73
|
}
|
|
@@ -99,16 +94,6 @@
|
|
|
99
94
|
return uniqInts(lines);
|
|
100
95
|
}
|
|
101
96
|
|
|
102
|
-
function isBlocked() {
|
|
103
|
-
return Date.now() < EZOIC_BLOCKED_UNTIL;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function softBlock(ms) {
|
|
107
|
-
const until = Date.now() + Math.max(0, ms || 0);
|
|
108
|
-
if (until > EZOIC_BLOCKED_UNTIL) EZOIC_BLOCKED_UNTIL = until;
|
|
109
|
-
debug('blocked', { until: EZOIC_BLOCKED_UNTIL });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
97
|
function getPageKey() {
|
|
113
98
|
try {
|
|
114
99
|
const ax = window.ajaxify;
|
|
@@ -153,12 +138,8 @@
|
|
|
153
138
|
});
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
function withInternalDomChange(fn) {
|
|
157
|
-
state.internalDomChange++;
|
|
158
|
-
try { return fn(); } finally { state.internalDomChange--; }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
141
|
// ---------- warm-up & patching ----------
|
|
142
|
+
|
|
162
143
|
const _warmLinksDone = new Set();
|
|
163
144
|
function warmUpNetwork() {
|
|
164
145
|
try {
|
|
@@ -225,7 +206,29 @@
|
|
|
225
206
|
}
|
|
226
207
|
}
|
|
227
208
|
|
|
228
|
-
|
|
209
|
+
const RECYCLE_COOLDOWN_MS = 1500;
|
|
210
|
+
|
|
211
|
+
function kindKeyFromClass(kindClass) {
|
|
212
|
+
if (kindClass === 'ezoic-ad-message') return 'topic';
|
|
213
|
+
if (kindClass === 'ezoic-ad-between') return 'categoryTopics';
|
|
214
|
+
if (kindClass === 'ezoic-ad-categories') return 'categories';
|
|
215
|
+
return 'topic';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function withInternalDomChange(fn) {
|
|
219
|
+
state.internalDomChange++;
|
|
220
|
+
try { fn(); } finally { state.internalDomChange--; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function canRecycle(kind) {
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const last = state.lastRecycleAt[kind] || 0;
|
|
226
|
+
if (now - last < RECYCLE_COOLDOWN_MS) return false;
|
|
227
|
+
state.lastRecycleAt[kind] = now;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
// ---------- config & pools ----------
|
|
231
|
+
|
|
229
232
|
async function fetchConfigOnce() {
|
|
230
233
|
if (state.cfg) return state.cfg;
|
|
231
234
|
try {
|
|
@@ -238,7 +241,7 @@
|
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
|
|
241
|
-
function
|
|
244
|
+
function initPools(cfg) {
|
|
242
245
|
if (!cfg) return;
|
|
243
246
|
if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
|
|
244
247
|
if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
@@ -246,6 +249,7 @@
|
|
|
246
249
|
}
|
|
247
250
|
|
|
248
251
|
// ---------- insertion primitives ----------
|
|
252
|
+
|
|
249
253
|
function isAdjacentAd(target) {
|
|
250
254
|
if (!target) return false;
|
|
251
255
|
const next = target.nextElementSibling;
|
|
@@ -255,9 +259,75 @@
|
|
|
255
259
|
return false;
|
|
256
260
|
}
|
|
257
261
|
|
|
258
|
-
|
|
262
|
+
|
|
263
|
+
function getWrapIdFromWrap(wrap) {
|
|
264
|
+
try {
|
|
265
|
+
const v = wrap.getAttribute('data-ezoic-wrapid');
|
|
266
|
+
if (v) return String(v);
|
|
267
|
+
const ph = wrap.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
268
|
+
if (ph && ph.id) return ph.id.replace(PLACEHOLDER_PREFIX, '');
|
|
269
|
+
} catch (e) {}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function safeDestroyById(id) {
|
|
274
|
+
try {
|
|
275
|
+
const ez = window.ezstandalone;
|
|
276
|
+
if (ez && typeof ez.destroyPlaceholders === 'function') {
|
|
277
|
+
ez.destroyPlaceholders([`${PLACEHOLDER_PREFIX}${id}`]);
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function pruneOrphanWraps(kindClass, items) {
|
|
283
|
+
if (!items || !items.length) return 0;
|
|
284
|
+
const itemSet = new Set(items);
|
|
285
|
+
const wraps = document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`);
|
|
286
|
+
let removed = 0;
|
|
287
|
+
|
|
288
|
+
wraps.forEach((wrap) => {
|
|
289
|
+
// NodeBB can insert separators/spacers; accept an anchor within a few previous siblings
|
|
290
|
+
let ok = false;
|
|
291
|
+
let prev = wrap.previousElementSibling;
|
|
292
|
+
for (let i = 0; i < 3 && prev; i++) {
|
|
293
|
+
if (itemSet.has(prev)) { ok = true; break; }
|
|
294
|
+
prev = prev.previousElementSibling;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!ok) {
|
|
298
|
+
const id = getWrapIdFromWrap(wrap);
|
|
299
|
+
withInternalDomChange(() => {
|
|
300
|
+
try {
|
|
301
|
+
if (id) safeDestroyById(id);
|
|
302
|
+
wrap.remove();
|
|
303
|
+
} catch (e) {}
|
|
304
|
+
});
|
|
305
|
+
removed++;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
310
|
+
return removed;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function refreshEmptyState(id) {
|
|
314
|
+
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
315
|
+
window.setTimeout(() => {
|
|
316
|
+
try {
|
|
317
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
318
|
+
if (!ph || !ph.isConnected) return;
|
|
319
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
320
|
+
if (!wrap) return;
|
|
321
|
+
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
322
|
+
if (hasContent) wrap.classList.remove('is-empty');
|
|
323
|
+
else wrap.classList.add('is-empty');
|
|
324
|
+
} catch (e) {}
|
|
325
|
+
}, 3500);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
259
329
|
const wrap = document.createElement('div');
|
|
260
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}
|
|
330
|
+
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
261
331
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
262
332
|
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
263
333
|
wrap.style.width = '100%';
|
|
@@ -277,68 +347,26 @@
|
|
|
277
347
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
278
348
|
if (!target || !target.insertAdjacentElement) return null;
|
|
279
349
|
if (findWrap(kindClass, afterPos)) return null;
|
|
350
|
+
if (insertingIds.has(id)) return null;
|
|
280
351
|
|
|
281
352
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
282
353
|
if (existingPh && existingPh.isConnected) return null;
|
|
283
354
|
|
|
284
|
-
|
|
355
|
+
insertingIds.add(id);
|
|
356
|
+
try {
|
|
285
357
|
const wrap = buildWrap(id, kindClass, afterPos);
|
|
286
358
|
target.insertAdjacentElement('afterend', wrap);
|
|
287
359
|
return wrap;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
function destroyPlaceholderId(id) {
|
|
292
|
-
try {
|
|
293
|
-
window.ezstandalone = window.ezstandalone || {};
|
|
294
|
-
const ez = window.ezstandalone;
|
|
295
|
-
if (typeof ez.destroyPlaceholders === 'function') {
|
|
296
|
-
try { ez.destroyPlaceholders(id); } catch (e) {}
|
|
297
|
-
} else if (ez.cmd && Array.isArray(ez.cmd)) {
|
|
298
|
-
ez.cmd.push(() => {
|
|
299
|
-
try {
|
|
300
|
-
if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
301
|
-
window.ezstandalone.destroyPlaceholders(id);
|
|
302
|
-
}
|
|
303
|
-
} catch (e) {}
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
} catch (e) {}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function canRecycle(kind) {
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
if (now - (state.lastRecycleAt[kind] || 0) < RECYCLE_COOLDOWN_MS) return false;
|
|
312
|
-
state.lastRecycleAt[kind] = now;
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function purgeOutsideWindow(kindClass, keepFrom, keepTo) {
|
|
317
|
-
// Remove wrappers too far away to free placeholder ids + reduce DOM bloat
|
|
318
|
-
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
319
|
-
if (!wraps.length) return 0;
|
|
320
|
-
|
|
321
|
-
let removed = 0;
|
|
322
|
-
withInternalDomChange(() => {
|
|
323
|
-
for (const w of wraps) {
|
|
324
|
-
const afterPos = parseInt(w.getAttribute('data-ezoic-after') || '', 10);
|
|
325
|
-
if (!Number.isFinite(afterPos)) continue;
|
|
326
|
-
if (afterPos >= keepFrom && afterPos <= keepTo) continue;
|
|
327
|
-
|
|
328
|
-
const id = parseInt(w.getAttribute('data-ezoic-wrapid') || '', 10);
|
|
329
|
-
if (Number.isFinite(id) && id > 0) destroyPlaceholderId(id);
|
|
330
|
-
|
|
331
|
-
try { w.remove(); removed++; } catch (e) {}
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
return removed;
|
|
360
|
+
} finally {
|
|
361
|
+
insertingIds.delete(id);
|
|
362
|
+
}
|
|
335
363
|
}
|
|
336
364
|
|
|
337
365
|
function pickIdFromAll(allIds, cursorKey) {
|
|
338
366
|
const n = allIds.length;
|
|
339
367
|
if (!n) return null;
|
|
340
368
|
|
|
341
|
-
// Try at most n
|
|
369
|
+
// Try at most n ids to find one that's not already in the DOM
|
|
342
370
|
for (let tries = 0; tries < n; tries++) {
|
|
343
371
|
const idx = state[cursorKey] % n;
|
|
344
372
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
@@ -346,24 +374,38 @@
|
|
|
346
374
|
const id = allIds[idx];
|
|
347
375
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
348
376
|
if (ph && ph.isConnected) continue;
|
|
377
|
+
|
|
349
378
|
return id;
|
|
350
379
|
}
|
|
351
380
|
return null;
|
|
352
381
|
}
|
|
353
382
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
383
|
+
|
|
384
|
+
function removeOneOldWrap(kindClass) {
|
|
385
|
+
try {
|
|
386
|
+
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
387
|
+
if (!wraps.length) return false;
|
|
388
|
+
|
|
389
|
+
// Prefer a wrap far above the viewport
|
|
390
|
+
let victim = null;
|
|
391
|
+
for (const w of wraps) {
|
|
392
|
+
const r = w.getBoundingClientRect();
|
|
393
|
+
if (r.bottom < -2000) { victim = w; break; }
|
|
394
|
+
}
|
|
395
|
+
// Otherwise remove the earliest one in the document
|
|
396
|
+
if (!victim) victim = wraps[0];
|
|
397
|
+
|
|
398
|
+
// Unobserve placeholder if still observed
|
|
357
399
|
try {
|
|
358
|
-
const ph =
|
|
359
|
-
if (
|
|
360
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
361
|
-
if (!wrap) return;
|
|
362
|
-
const hasContent = ph.childElementCount > 0;
|
|
363
|
-
if (hasContent) wrap.classList.remove('is-empty');
|
|
364
|
-
else wrap.classList.add('is-empty');
|
|
400
|
+
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
401
|
+
if (ph && state.io) state.io.unobserve(ph);
|
|
365
402
|
} catch (e) {}
|
|
366
|
-
|
|
403
|
+
|
|
404
|
+
victim.remove();
|
|
405
|
+
return true;
|
|
406
|
+
} catch (e) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
367
409
|
}
|
|
368
410
|
|
|
369
411
|
function showAd(id) {
|
|
@@ -373,27 +415,36 @@
|
|
|
373
415
|
const last = state.lastShowById.get(id) || 0;
|
|
374
416
|
if (now - last < 1500) return; // basic throttle
|
|
375
417
|
|
|
376
|
-
|
|
418
|
+
// Defer one frame so the placeholder is definitely in DOM after insertion/recycle
|
|
419
|
+
requestAnimationFrame(() => {
|
|
377
420
|
if (isBlocked()) return;
|
|
421
|
+
|
|
378
422
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
379
423
|
if (!ph || !ph.isConnected) return;
|
|
380
424
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
386
|
-
if (wrap) wrap.classList.add('is-empty');
|
|
387
|
-
} catch (e) {}
|
|
425
|
+
const now2 = Date.now();
|
|
426
|
+
const last2 = state.lastShowById.get(id) || 0;
|
|
427
|
+
if (now2 - last2 < 1200) return;
|
|
428
|
+
state.lastShowById.set(id, now2);
|
|
388
429
|
|
|
389
430
|
try {
|
|
390
431
|
window.ezstandalone = window.ezstandalone || {};
|
|
391
432
|
const ez = window.ezstandalone;
|
|
392
433
|
|
|
434
|
+
const doShow = () => {
|
|
435
|
+
try {
|
|
436
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
437
|
+
// Avoid Ezoic caching state for reused placeholders
|
|
438
|
+
ez.destroyPlaceholders(id);
|
|
439
|
+
}
|
|
440
|
+
} catch (e) {}
|
|
441
|
+
try { ez.showAds(id); } catch (e) {}
|
|
442
|
+
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
443
|
+
};
|
|
444
|
+
|
|
393
445
|
// Fast path
|
|
394
446
|
if (typeof ez.showAds === 'function') {
|
|
395
|
-
|
|
396
|
-
markEmptyLater(id);
|
|
447
|
+
doShow();
|
|
397
448
|
return;
|
|
398
449
|
}
|
|
399
450
|
|
|
@@ -406,19 +457,25 @@
|
|
|
406
457
|
if (isBlocked()) return;
|
|
407
458
|
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
408
459
|
if (!el || !el.isConnected) return;
|
|
409
|
-
window.ezstandalone
|
|
410
|
-
|
|
460
|
+
const ez2 = window.ezstandalone;
|
|
461
|
+
if (!ez2 || typeof ez2.showAds !== 'function') return;
|
|
462
|
+
try {
|
|
463
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez2.destroyPlaceholders === 'function') {
|
|
464
|
+
ez2.destroyPlaceholders(id);
|
|
465
|
+
}
|
|
466
|
+
} catch (e) {}
|
|
467
|
+
try { ez2.showAds(id); } catch (e) {}
|
|
468
|
+
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
411
469
|
} catch (e) {}
|
|
412
470
|
});
|
|
413
471
|
}
|
|
414
472
|
} catch (e) {}
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Defer one frame to reduce "element does not exist" warnings
|
|
418
|
-
window.requestAnimationFrame(doShow);
|
|
473
|
+
});
|
|
419
474
|
}
|
|
420
475
|
|
|
476
|
+
|
|
421
477
|
// ---------- preload / above-the-fold ----------
|
|
478
|
+
|
|
422
479
|
function ensurePreloadObserver() {
|
|
423
480
|
if (state.io) return state.io;
|
|
424
481
|
try {
|
|
@@ -452,72 +509,22 @@
|
|
|
452
509
|
} catch (e) {}
|
|
453
510
|
}
|
|
454
511
|
|
|
455
|
-
// ----------
|
|
456
|
-
function estimateWindowItems(interval, idsCount) {
|
|
457
|
-
// Ensure we can roughly keep "every X" within the window using available ids
|
|
458
|
-
// If ids are low, keep a smaller window to avoid constant recycling.
|
|
459
|
-
const base = Math.max(WINDOW_MIN_ITEMS, Math.floor((idsCount || 1) * Math.max(1, interval) * 1.1));
|
|
460
|
-
return Math.min(WINDOW_MAX_ITEMS, base);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function getVisibleRange(items) {
|
|
464
|
-
// Returns [startIdx, endIdx] (0-based, inclusive) for items near viewport
|
|
465
|
-
let start = 0;
|
|
466
|
-
let end = items.length - 1;
|
|
467
|
-
let foundAny = false;
|
|
468
|
-
|
|
469
|
-
const topBound = -WINDOW_BUFFER_PX;
|
|
470
|
-
const bottomBound = window.innerHeight + WINDOW_BUFFER_PX;
|
|
471
|
-
|
|
472
|
-
for (let i = 0; i < items.length; i++) {
|
|
473
|
-
const el = items[i];
|
|
474
|
-
if (!el || !el.isConnected) continue;
|
|
475
|
-
let r;
|
|
476
|
-
try { r = el.getBoundingClientRect(); } catch (e) { continue; }
|
|
477
|
-
if (r.bottom < topBound) { start = i + 1; continue; }
|
|
478
|
-
if (r.top > bottomBound) { end = i - 1; break; }
|
|
479
|
-
foundAny = true;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!foundAny) return [0, Math.min(items.length - 1, 30)];
|
|
483
|
-
start = Math.max(0, Math.min(start, items.length - 1));
|
|
484
|
-
end = Math.max(start, Math.min(end, items.length - 1));
|
|
485
|
-
return [start, end];
|
|
486
|
-
}
|
|
512
|
+
// ---------- insertion logic ----------
|
|
487
513
|
|
|
488
|
-
function
|
|
514
|
+
function computeTargets(count, interval, showFirst) {
|
|
489
515
|
const out = [];
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return out;
|
|
516
|
+
if (count <= 0) return out;
|
|
517
|
+
if (showFirst) out.push(1);
|
|
518
|
+
for (let i = 1; i <= count; i++) {
|
|
519
|
+
if (i % interval === 0) out.push(i);
|
|
520
|
+
}
|
|
521
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
498
522
|
}
|
|
499
523
|
|
|
500
|
-
function
|
|
524
|
+
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
501
525
|
if (!items.length) return 0;
|
|
502
526
|
|
|
503
|
-
const
|
|
504
|
-
const windowItems = estimateWindowItems(interval, idsCount);
|
|
505
|
-
|
|
506
|
-
const [visStart, visEnd] = getVisibleRange(items);
|
|
507
|
-
const mid = Math.floor((visStart + visEnd) / 2);
|
|
508
|
-
const winStartIdx = Math.max(0, mid - Math.floor(windowItems / 2));
|
|
509
|
-
const winEndIdx = Math.min(items.length - 1, winStartIdx + windowItems - 1);
|
|
510
|
-
|
|
511
|
-
const keepFrom = Math.max(1, (winStartIdx + 1) - PURGE_BUFFER_ITEMS);
|
|
512
|
-
const keepTo = Math.min(items.length, (winEndIdx + 1) + PURGE_BUFFER_ITEMS);
|
|
513
|
-
|
|
514
|
-
const purged = purgeOutsideWindow(kindClass, keepFrom, keepTo);
|
|
515
|
-
if (purged) debug('purge', kindClass, { purged, keepFrom, keepTo });
|
|
516
|
-
|
|
517
|
-
const fromPos = winStartIdx + 1;
|
|
518
|
-
const toPos = winEndIdx + 1;
|
|
519
|
-
|
|
520
|
-
const targets = computeTargetsInRange(fromPos, toPos, interval, showFirst);
|
|
527
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
521
528
|
let inserted = 0;
|
|
522
529
|
|
|
523
530
|
for (const afterPos of targets) {
|
|
@@ -529,25 +536,25 @@
|
|
|
529
536
|
if (findWrap(kindClass, afterPos)) continue;
|
|
530
537
|
|
|
531
538
|
let id = pickIdFromAll(allIds, cursorKey);
|
|
532
|
-
|
|
533
|
-
// If no ID is currently free, try a purge (only occasionally) then try again
|
|
534
539
|
if (!id) {
|
|
535
|
-
|
|
536
|
-
|
|
540
|
+
// No free ids: recycle an old ad wrapper so we can reuse its placeholder id.
|
|
541
|
+
// Guard against tight observer loops.
|
|
542
|
+
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
543
|
+
dbg('recycle-skip-cooldown', kindClass);
|
|
537
544
|
break;
|
|
538
545
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
//
|
|
545
|
-
softBlock(400);
|
|
546
|
+
let recycled = false;
|
|
547
|
+
withInternalDomChange(() => {
|
|
548
|
+
recycled = removeOneOldWrap(kindClass);
|
|
549
|
+
});
|
|
550
|
+
dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
|
|
551
|
+
// Stop this run after a recycle; the next mutation/scroll will retry injection.
|
|
546
552
|
break;
|
|
547
553
|
}
|
|
548
|
-
|
|
549
554
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
550
|
-
if (!wrap)
|
|
555
|
+
if (!wrap) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
551
558
|
|
|
552
559
|
observePlaceholder(id);
|
|
553
560
|
inserted += 1;
|
|
@@ -559,9 +566,10 @@
|
|
|
559
566
|
async function insertHeroAdEarly() {
|
|
560
567
|
if (state.heroDoneForPage) return;
|
|
561
568
|
const cfg = await fetchConfigOnce();
|
|
562
|
-
if (!cfg
|
|
569
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
570
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
563
571
|
|
|
564
|
-
|
|
572
|
+
initPools(cfg);
|
|
565
573
|
|
|
566
574
|
const kind = getKind();
|
|
567
575
|
let items = [];
|
|
@@ -571,32 +579,31 @@
|
|
|
571
579
|
let showFirst = false;
|
|
572
580
|
|
|
573
581
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
574
|
-
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
575
|
-
if (!showFirst) return;
|
|
576
582
|
items = getPostContainers();
|
|
577
583
|
allIds = state.allPosts;
|
|
578
584
|
cursorKey = 'curPosts';
|
|
579
585
|
kindClass = 'ezoic-ad-message';
|
|
586
|
+
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
580
587
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
581
|
-
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
582
|
-
if (!showFirst) return;
|
|
583
588
|
items = getTopicItems();
|
|
584
589
|
allIds = state.allTopics;
|
|
585
590
|
cursorKey = 'curTopics';
|
|
586
591
|
kindClass = 'ezoic-ad-between';
|
|
592
|
+
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
587
593
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
588
|
-
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
589
|
-
if (!showFirst) return;
|
|
590
594
|
items = getCategoryItems();
|
|
591
595
|
allIds = state.allCategories;
|
|
592
596
|
cursorKey = 'curCategories';
|
|
593
597
|
kindClass = 'ezoic-ad-categories';
|
|
598
|
+
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
594
599
|
} else {
|
|
595
600
|
return;
|
|
596
601
|
}
|
|
597
602
|
|
|
598
603
|
if (!items.length) return;
|
|
604
|
+
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
599
605
|
|
|
606
|
+
// Insert after the very first item (above-the-fold)
|
|
600
607
|
const afterPos = 1;
|
|
601
608
|
const el = items[afterPos - 1];
|
|
602
609
|
if (!el || !el.isConnected) return;
|
|
@@ -607,29 +614,33 @@
|
|
|
607
614
|
if (!id) return;
|
|
608
615
|
|
|
609
616
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
610
|
-
if (!wrap)
|
|
617
|
+
if (!wrap) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
611
620
|
|
|
612
621
|
state.heroDoneForPage = true;
|
|
613
622
|
observePlaceholder(id);
|
|
614
623
|
}
|
|
615
624
|
|
|
616
625
|
async function runCore() {
|
|
617
|
-
if (isBlocked()) return;
|
|
626
|
+
if (isBlocked()) { dbg('blocked'); return; }
|
|
618
627
|
|
|
619
628
|
patchShowAds();
|
|
620
629
|
|
|
621
630
|
const cfg = await fetchConfigOnce();
|
|
622
|
-
if (!cfg
|
|
623
|
-
|
|
631
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
632
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
633
|
+
initPools(cfg);
|
|
624
634
|
|
|
625
635
|
const kind = getKind();
|
|
626
636
|
|
|
627
637
|
if (kind === 'topic') {
|
|
628
638
|
if (normalizeBool(cfg.enableMessageAds)) {
|
|
629
|
-
|
|
630
|
-
|
|
639
|
+
const __items = getPostContainers();
|
|
640
|
+
pruneOrphanWraps('ezoic-ad-message', __items);
|
|
641
|
+
injectBetween(
|
|
631
642
|
'ezoic-ad-message',
|
|
632
|
-
|
|
643
|
+
__items,
|
|
633
644
|
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
634
645
|
normalizeBool(cfg.showFirstMessageAd),
|
|
635
646
|
state.allPosts,
|
|
@@ -638,10 +649,11 @@
|
|
|
638
649
|
}
|
|
639
650
|
} else if (kind === 'categoryTopics') {
|
|
640
651
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
641
|
-
|
|
642
|
-
|
|
652
|
+
const __items = getTopicItems();
|
|
653
|
+
pruneOrphanWraps('ezoic-ad-between', __items);
|
|
654
|
+
injectBetween(
|
|
643
655
|
'ezoic-ad-between',
|
|
644
|
-
|
|
656
|
+
__items,
|
|
645
657
|
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
646
658
|
normalizeBool(cfg.showFirstTopicAd),
|
|
647
659
|
state.allTopics,
|
|
@@ -650,10 +662,11 @@
|
|
|
650
662
|
}
|
|
651
663
|
} else if (kind === 'categories') {
|
|
652
664
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
653
|
-
|
|
654
|
-
|
|
665
|
+
const __items = getCategoryItems();
|
|
666
|
+
pruneOrphanWraps('ezoic-ad-categories', __items);
|
|
667
|
+
injectBetween(
|
|
655
668
|
'ezoic-ad-categories',
|
|
656
|
-
|
|
669
|
+
__items,
|
|
657
670
|
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
658
671
|
normalizeBool(cfg.showFirstCategoryAd),
|
|
659
672
|
state.allCategories,
|
|
@@ -675,19 +688,14 @@
|
|
|
675
688
|
}
|
|
676
689
|
|
|
677
690
|
// ---------- observers / lifecycle ----------
|
|
691
|
+
|
|
678
692
|
function cleanup() {
|
|
679
|
-
|
|
693
|
+
blockedUntil = Date.now() + 1200;
|
|
680
694
|
|
|
681
695
|
// remove all wrappers
|
|
682
696
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
const id = parseInt(el.getAttribute('data-ezoic-wrapid') || '', 10);
|
|
687
|
-
if (Number.isFinite(id) && id > 0) destroyPlaceholderId(id);
|
|
688
|
-
} catch (e) {}
|
|
689
|
-
try { el.remove(); } catch (e) {}
|
|
690
|
-
});
|
|
697
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
698
|
+
try { el.remove(); } catch (e) {}
|
|
691
699
|
});
|
|
692
700
|
} catch (e) {}
|
|
693
701
|
|
|
@@ -699,20 +707,18 @@
|
|
|
699
707
|
state.curTopics = 0;
|
|
700
708
|
state.curPosts = 0;
|
|
701
709
|
state.curCategories = 0;
|
|
702
|
-
|
|
703
710
|
state.lastShowById.clear();
|
|
711
|
+
try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
|
|
704
712
|
state.heroDoneForPage = false;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
state.lastRecycleAt.categories = 0;
|
|
713
|
+
|
|
714
|
+
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
708
715
|
}
|
|
709
716
|
|
|
710
717
|
function ensureDomObserver() {
|
|
711
718
|
if (state.domObs) return;
|
|
712
719
|
state.domObs = new MutationObserver(() => {
|
|
713
720
|
if (state.internalDomChange > 0) return;
|
|
714
|
-
if (isBlocked())
|
|
715
|
-
scheduleRun();
|
|
721
|
+
if (!isBlocked()) scheduleRun();
|
|
716
722
|
});
|
|
717
723
|
try {
|
|
718
724
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
@@ -730,7 +736,7 @@
|
|
|
730
736
|
|
|
731
737
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
732
738
|
state.pageKey = getPageKey();
|
|
733
|
-
|
|
739
|
+
blockedUntil = 0;
|
|
734
740
|
|
|
735
741
|
warmUpNetwork();
|
|
736
742
|
patchShowAds();
|
|
@@ -764,6 +770,7 @@
|
|
|
764
770
|
}
|
|
765
771
|
|
|
766
772
|
// ---------- boot ----------
|
|
773
|
+
|
|
767
774
|
state.pageKey = getPageKey();
|
|
768
775
|
warmUpNetwork();
|
|
769
776
|
patchShowAds();
|
|
@@ -774,7 +781,7 @@
|
|
|
774
781
|
bindScroll();
|
|
775
782
|
|
|
776
783
|
// First paint: try hero + run
|
|
777
|
-
|
|
784
|
+
blockedUntil = 0;
|
|
778
785
|
insertHeroAdEarly().catch(() => {});
|
|
779
786
|
scheduleRun();
|
|
780
|
-
})();
|
|
787
|
+
})();
|
package/public/style.css
CHANGED
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
25
|
.ezoic-ad.is-empty {
|
|
26
|
-
display:
|
|
26
|
+
display: block !important;
|
|
27
27
|
margin: 0 !important;
|
|
28
28
|
padding: 0 !important;
|
|
29
29
|
height: 0 !important;
|
|
30
30
|
min-height: 0 !important;
|
|
31
|
+
overflow: hidden !important;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
.ezoic-ad {
|