nodebb-plugin-ezoic-infinite 1.5.28 → 1.5.29
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 +234 -234
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,7 +259,66 @@
|
|
|
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
|
+
const prev = wrap.previousElementSibling;
|
|
290
|
+
if (!prev || !itemSet.has(prev)) {
|
|
291
|
+
const id = getWrapIdFromWrap(wrap);
|
|
292
|
+
withInternalDomChange(() => {
|
|
293
|
+
try {
|
|
294
|
+
if (id) safeDestroyById(id);
|
|
295
|
+
wrap.remove();
|
|
296
|
+
} catch (e) {}
|
|
297
|
+
});
|
|
298
|
+
removed++;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (removed) dbg('prune-orphan', kindClass, { removed });
|
|
303
|
+
return removed;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function refreshEmptyState(id) {
|
|
307
|
+
// After Ezoic has had a moment to fill the placeholder, toggle the CSS class.
|
|
308
|
+
window.setTimeout(() => {
|
|
309
|
+
try {
|
|
310
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
311
|
+
if (!ph || !ph.isConnected) return;
|
|
312
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
313
|
+
if (!wrap) return;
|
|
314
|
+
const hasContent = ph.childElementCount > 0 || ph.offsetHeight > 0;
|
|
315
|
+
if (hasContent) wrap.classList.remove('is-empty');
|
|
316
|
+
else wrap.classList.add('is-empty');
|
|
317
|
+
} catch (e) {}
|
|
318
|
+
}, 1800);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildWrap(id, kindClass, afterPos) {
|
|
259
322
|
const wrap = document.createElement('div');
|
|
260
323
|
wrap.className = `${WRAP_CLASS} ${kindClass} is-empty`;
|
|
261
324
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
@@ -277,68 +340,26 @@
|
|
|
277
340
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
278
341
|
if (!target || !target.insertAdjacentElement) return null;
|
|
279
342
|
if (findWrap(kindClass, afterPos)) return null;
|
|
343
|
+
if (insertingIds.has(id)) return null;
|
|
280
344
|
|
|
281
345
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
282
346
|
if (existingPh && existingPh.isConnected) return null;
|
|
283
347
|
|
|
284
|
-
|
|
348
|
+
insertingIds.add(id);
|
|
349
|
+
try {
|
|
285
350
|
const wrap = buildWrap(id, kindClass, afterPos);
|
|
286
351
|
target.insertAdjacentElement('afterend', wrap);
|
|
287
352
|
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;
|
|
353
|
+
} finally {
|
|
354
|
+
insertingIds.delete(id);
|
|
355
|
+
}
|
|
335
356
|
}
|
|
336
357
|
|
|
337
358
|
function pickIdFromAll(allIds, cursorKey) {
|
|
338
359
|
const n = allIds.length;
|
|
339
360
|
if (!n) return null;
|
|
340
361
|
|
|
341
|
-
// Try at most n
|
|
362
|
+
// Try at most n ids to find one that's not already in the DOM
|
|
342
363
|
for (let tries = 0; tries < n; tries++) {
|
|
343
364
|
const idx = state[cursorKey] % n;
|
|
344
365
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
@@ -346,24 +367,38 @@
|
|
|
346
367
|
const id = allIds[idx];
|
|
347
368
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
348
369
|
if (ph && ph.isConnected) continue;
|
|
370
|
+
|
|
349
371
|
return id;
|
|
350
372
|
}
|
|
351
373
|
return null;
|
|
352
374
|
}
|
|
353
375
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
376
|
+
|
|
377
|
+
function removeOneOldWrap(kindClass) {
|
|
378
|
+
try {
|
|
379
|
+
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
380
|
+
if (!wraps.length) return false;
|
|
381
|
+
|
|
382
|
+
// Prefer a wrap far above the viewport
|
|
383
|
+
let victim = null;
|
|
384
|
+
for (const w of wraps) {
|
|
385
|
+
const r = w.getBoundingClientRect();
|
|
386
|
+
if (r.bottom < -2000) { victim = w; break; }
|
|
387
|
+
}
|
|
388
|
+
// Otherwise remove the earliest one in the document
|
|
389
|
+
if (!victim) victim = wraps[0];
|
|
390
|
+
|
|
391
|
+
// Unobserve placeholder if still observed
|
|
357
392
|
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');
|
|
393
|
+
const ph = victim.querySelector && victim.querySelector(`[id^="${PLACEHOLDER_PREFIX}"]`);
|
|
394
|
+
if (ph && state.io) state.io.unobserve(ph);
|
|
365
395
|
} catch (e) {}
|
|
366
|
-
|
|
396
|
+
|
|
397
|
+
victim.remove();
|
|
398
|
+
return true;
|
|
399
|
+
} catch (e) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
367
402
|
}
|
|
368
403
|
|
|
369
404
|
function showAd(id) {
|
|
@@ -373,27 +408,36 @@
|
|
|
373
408
|
const last = state.lastShowById.get(id) || 0;
|
|
374
409
|
if (now - last < 1500) return; // basic throttle
|
|
375
410
|
|
|
376
|
-
|
|
411
|
+
// Defer one frame so the placeholder is definitely in DOM after insertion/recycle
|
|
412
|
+
requestAnimationFrame(() => {
|
|
377
413
|
if (isBlocked()) return;
|
|
414
|
+
|
|
378
415
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
379
416
|
if (!ph || !ph.isConnected) return;
|
|
380
417
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
386
|
-
if (wrap) wrap.classList.add('is-empty');
|
|
387
|
-
} catch (e) {}
|
|
418
|
+
const now2 = Date.now();
|
|
419
|
+
const last2 = state.lastShowById.get(id) || 0;
|
|
420
|
+
if (now2 - last2 < 1200) return;
|
|
421
|
+
state.lastShowById.set(id, now2);
|
|
388
422
|
|
|
389
423
|
try {
|
|
390
424
|
window.ezstandalone = window.ezstandalone || {};
|
|
391
425
|
const ez = window.ezstandalone;
|
|
392
426
|
|
|
427
|
+
const doShow = () => {
|
|
428
|
+
try {
|
|
429
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
430
|
+
// Avoid Ezoic caching state for reused placeholders
|
|
431
|
+
ez.destroyPlaceholders(id);
|
|
432
|
+
}
|
|
433
|
+
} catch (e) {}
|
|
434
|
+
try { ez.showAds(id); } catch (e) {}
|
|
435
|
+
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
436
|
+
};
|
|
437
|
+
|
|
393
438
|
// Fast path
|
|
394
439
|
if (typeof ez.showAds === 'function') {
|
|
395
|
-
|
|
396
|
-
markEmptyLater(id);
|
|
440
|
+
doShow();
|
|
397
441
|
return;
|
|
398
442
|
}
|
|
399
443
|
|
|
@@ -406,19 +450,25 @@
|
|
|
406
450
|
if (isBlocked()) return;
|
|
407
451
|
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
408
452
|
if (!el || !el.isConnected) return;
|
|
409
|
-
window.ezstandalone
|
|
410
|
-
|
|
453
|
+
const ez2 = window.ezstandalone;
|
|
454
|
+
if (!ez2 || typeof ez2.showAds !== 'function') return;
|
|
455
|
+
try {
|
|
456
|
+
if (state.usedOnce && state.usedOnce.has(id) && typeof ez2.destroyPlaceholders === 'function') {
|
|
457
|
+
ez2.destroyPlaceholders(id);
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {}
|
|
460
|
+
try { ez2.showAds(id); } catch (e) {}
|
|
461
|
+
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
411
462
|
} catch (e) {}
|
|
412
463
|
});
|
|
413
464
|
}
|
|
414
465
|
} catch (e) {}
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Defer one frame to reduce "element does not exist" warnings
|
|
418
|
-
window.requestAnimationFrame(doShow);
|
|
466
|
+
});
|
|
419
467
|
}
|
|
420
468
|
|
|
469
|
+
|
|
421
470
|
// ---------- preload / above-the-fold ----------
|
|
471
|
+
|
|
422
472
|
function ensurePreloadObserver() {
|
|
423
473
|
if (state.io) return state.io;
|
|
424
474
|
try {
|
|
@@ -452,72 +502,22 @@
|
|
|
452
502
|
} catch (e) {}
|
|
453
503
|
}
|
|
454
504
|
|
|
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
|
-
}
|
|
505
|
+
// ---------- insertion logic ----------
|
|
487
506
|
|
|
488
|
-
function
|
|
507
|
+
function computeTargets(count, interval, showFirst) {
|
|
489
508
|
const out = [];
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return out;
|
|
509
|
+
if (count <= 0) return out;
|
|
510
|
+
if (showFirst) out.push(1);
|
|
511
|
+
for (let i = 1; i <= count; i++) {
|
|
512
|
+
if (i % interval === 0) out.push(i);
|
|
513
|
+
}
|
|
514
|
+
return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
498
515
|
}
|
|
499
516
|
|
|
500
|
-
function
|
|
517
|
+
function injectBetween(kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
501
518
|
if (!items.length) return 0;
|
|
502
519
|
|
|
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);
|
|
520
|
+
const targets = computeTargets(items.length, interval, showFirst);
|
|
521
521
|
let inserted = 0;
|
|
522
522
|
|
|
523
523
|
for (const afterPos of targets) {
|
|
@@ -529,25 +529,25 @@
|
|
|
529
529
|
if (findWrap(kindClass, afterPos)) continue;
|
|
530
530
|
|
|
531
531
|
let id = pickIdFromAll(allIds, cursorKey);
|
|
532
|
-
|
|
533
|
-
// If no ID is currently free, try a purge (only occasionally) then try again
|
|
534
532
|
if (!id) {
|
|
535
|
-
|
|
536
|
-
|
|
533
|
+
// No free ids: recycle an old ad wrapper so we can reuse its placeholder id.
|
|
534
|
+
// Guard against tight observer loops.
|
|
535
|
+
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
536
|
+
dbg('recycle-skip-cooldown', kindClass);
|
|
537
537
|
break;
|
|
538
538
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
//
|
|
545
|
-
softBlock(400);
|
|
539
|
+
let recycled = false;
|
|
540
|
+
withInternalDomChange(() => {
|
|
541
|
+
recycled = removeOneOldWrap(kindClass);
|
|
542
|
+
});
|
|
543
|
+
dbg('recycle-needed', kindClass, { recycled, ids: allIds.length });
|
|
544
|
+
// Stop this run after a recycle; the next mutation/scroll will retry injection.
|
|
546
545
|
break;
|
|
547
546
|
}
|
|
548
|
-
|
|
549
547
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
550
|
-
if (!wrap)
|
|
548
|
+
if (!wrap) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
551
|
|
|
552
552
|
observePlaceholder(id);
|
|
553
553
|
inserted += 1;
|
|
@@ -559,9 +559,10 @@
|
|
|
559
559
|
async function insertHeroAdEarly() {
|
|
560
560
|
if (state.heroDoneForPage) return;
|
|
561
561
|
const cfg = await fetchConfigOnce();
|
|
562
|
-
if (!cfg
|
|
562
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
563
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
563
564
|
|
|
564
|
-
|
|
565
|
+
initPools(cfg);
|
|
565
566
|
|
|
566
567
|
const kind = getKind();
|
|
567
568
|
let items = [];
|
|
@@ -571,32 +572,31 @@
|
|
|
571
572
|
let showFirst = false;
|
|
572
573
|
|
|
573
574
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
574
|
-
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
575
|
-
if (!showFirst) return;
|
|
576
575
|
items = getPostContainers();
|
|
577
576
|
allIds = state.allPosts;
|
|
578
577
|
cursorKey = 'curPosts';
|
|
579
578
|
kindClass = 'ezoic-ad-message';
|
|
579
|
+
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
580
580
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
581
|
-
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
582
|
-
if (!showFirst) return;
|
|
583
581
|
items = getTopicItems();
|
|
584
582
|
allIds = state.allTopics;
|
|
585
583
|
cursorKey = 'curTopics';
|
|
586
584
|
kindClass = 'ezoic-ad-between';
|
|
585
|
+
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
587
586
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
588
|
-
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
589
|
-
if (!showFirst) return;
|
|
590
587
|
items = getCategoryItems();
|
|
591
588
|
allIds = state.allCategories;
|
|
592
589
|
cursorKey = 'curCategories';
|
|
593
590
|
kindClass = 'ezoic-ad-categories';
|
|
591
|
+
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
594
592
|
} else {
|
|
595
593
|
return;
|
|
596
594
|
}
|
|
597
595
|
|
|
598
596
|
if (!items.length) return;
|
|
597
|
+
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
599
598
|
|
|
599
|
+
// Insert after the very first item (above-the-fold)
|
|
600
600
|
const afterPos = 1;
|
|
601
601
|
const el = items[afterPos - 1];
|
|
602
602
|
if (!el || !el.isConnected) return;
|
|
@@ -607,29 +607,33 @@
|
|
|
607
607
|
if (!id) return;
|
|
608
608
|
|
|
609
609
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
610
|
-
if (!wrap)
|
|
610
|
+
if (!wrap) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
611
613
|
|
|
612
614
|
state.heroDoneForPage = true;
|
|
613
615
|
observePlaceholder(id);
|
|
614
616
|
}
|
|
615
617
|
|
|
616
618
|
async function runCore() {
|
|
617
|
-
if (isBlocked()) return;
|
|
619
|
+
if (isBlocked()) { dbg('blocked'); return; }
|
|
618
620
|
|
|
619
621
|
patchShowAds();
|
|
620
622
|
|
|
621
623
|
const cfg = await fetchConfigOnce();
|
|
622
|
-
if (!cfg
|
|
623
|
-
|
|
624
|
+
if (!cfg) { dbg('no-config'); return; }
|
|
625
|
+
if (cfg.excluded) { dbg('excluded'); return; }
|
|
626
|
+
initPools(cfg);
|
|
624
627
|
|
|
625
628
|
const kind = getKind();
|
|
626
629
|
|
|
627
630
|
if (kind === 'topic') {
|
|
628
631
|
if (normalizeBool(cfg.enableMessageAds)) {
|
|
629
|
-
|
|
630
|
-
|
|
632
|
+
const __items = getPostContainers();
|
|
633
|
+
pruneOrphanWraps('ezoic-ad-message', __items);
|
|
634
|
+
injectBetween(
|
|
631
635
|
'ezoic-ad-message',
|
|
632
|
-
|
|
636
|
+
__items,
|
|
633
637
|
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
634
638
|
normalizeBool(cfg.showFirstMessageAd),
|
|
635
639
|
state.allPosts,
|
|
@@ -638,10 +642,11 @@
|
|
|
638
642
|
}
|
|
639
643
|
} else if (kind === 'categoryTopics') {
|
|
640
644
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
641
|
-
|
|
642
|
-
|
|
645
|
+
const __items = getTopicItems();
|
|
646
|
+
pruneOrphanWraps('ezoic-ad-between', __items);
|
|
647
|
+
injectBetween(
|
|
643
648
|
'ezoic-ad-between',
|
|
644
|
-
|
|
649
|
+
__items,
|
|
645
650
|
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
646
651
|
normalizeBool(cfg.showFirstTopicAd),
|
|
647
652
|
state.allTopics,
|
|
@@ -650,10 +655,11 @@
|
|
|
650
655
|
}
|
|
651
656
|
} else if (kind === 'categories') {
|
|
652
657
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
653
|
-
|
|
654
|
-
|
|
658
|
+
const __items = getCategoryItems();
|
|
659
|
+
pruneOrphanWraps('ezoic-ad-categories', __items);
|
|
660
|
+
injectBetween(
|
|
655
661
|
'ezoic-ad-categories',
|
|
656
|
-
|
|
662
|
+
__items,
|
|
657
663
|
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
658
664
|
normalizeBool(cfg.showFirstCategoryAd),
|
|
659
665
|
state.allCategories,
|
|
@@ -675,19 +681,14 @@
|
|
|
675
681
|
}
|
|
676
682
|
|
|
677
683
|
// ---------- observers / lifecycle ----------
|
|
684
|
+
|
|
678
685
|
function cleanup() {
|
|
679
|
-
|
|
686
|
+
blockedUntil = Date.now() + 1200;
|
|
680
687
|
|
|
681
688
|
// remove all wrappers
|
|
682
689
|
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
|
-
});
|
|
690
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
691
|
+
try { el.remove(); } catch (e) {}
|
|
691
692
|
});
|
|
692
693
|
} catch (e) {}
|
|
693
694
|
|
|
@@ -699,20 +700,18 @@
|
|
|
699
700
|
state.curTopics = 0;
|
|
700
701
|
state.curPosts = 0;
|
|
701
702
|
state.curCategories = 0;
|
|
702
|
-
|
|
703
703
|
state.lastShowById.clear();
|
|
704
|
+
try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
|
|
704
705
|
state.heroDoneForPage = false;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
state.lastRecycleAt.categories = 0;
|
|
706
|
+
|
|
707
|
+
// keep observers alive (MutationObserver will re-trigger after navigation)
|
|
708
708
|
}
|
|
709
709
|
|
|
710
710
|
function ensureDomObserver() {
|
|
711
711
|
if (state.domObs) return;
|
|
712
712
|
state.domObs = new MutationObserver(() => {
|
|
713
713
|
if (state.internalDomChange > 0) return;
|
|
714
|
-
if (isBlocked())
|
|
715
|
-
scheduleRun();
|
|
714
|
+
if (!isBlocked()) scheduleRun();
|
|
716
715
|
});
|
|
717
716
|
try {
|
|
718
717
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
@@ -730,7 +729,7 @@
|
|
|
730
729
|
|
|
731
730
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
732
731
|
state.pageKey = getPageKey();
|
|
733
|
-
|
|
732
|
+
blockedUntil = 0;
|
|
734
733
|
|
|
735
734
|
warmUpNetwork();
|
|
736
735
|
patchShowAds();
|
|
@@ -764,6 +763,7 @@
|
|
|
764
763
|
}
|
|
765
764
|
|
|
766
765
|
// ---------- boot ----------
|
|
766
|
+
|
|
767
767
|
state.pageKey = getPageKey();
|
|
768
768
|
warmUpNetwork();
|
|
769
769
|
patchShowAds();
|
|
@@ -774,7 +774,7 @@
|
|
|
774
774
|
bindScroll();
|
|
775
775
|
|
|
776
776
|
// First paint: try hero + run
|
|
777
|
-
|
|
777
|
+
blockedUntil = 0;
|
|
778
778
|
insertHeroAdEarly().catch(() => {});
|
|
779
779
|
scheduleRun();
|
|
780
|
-
})();
|
|
780
|
+
})();
|