nodebb-plugin-ezoic-infinite 1.5.26 → 1.5.28
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 +261 -145
- package/public/style.css +18 -0
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -13,20 +13,27 @@
|
|
|
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
|
+
|
|
16
26
|
const SELECTORS = {
|
|
17
27
|
topicItem: 'li[component="category/topic"]',
|
|
18
28
|
postItem: '[component="post"][data-pid]',
|
|
19
29
|
categoryItem: 'li[component="categories/category"]',
|
|
20
30
|
};
|
|
21
31
|
|
|
22
|
-
// Hard block during navigation to avoid “placeholder does not exist” spam
|
|
23
|
-
let EZOIC_BLOCKED = false;
|
|
24
|
-
|
|
25
32
|
const state = {
|
|
26
33
|
pageKey: null,
|
|
27
34
|
cfg: null,
|
|
28
35
|
|
|
29
|
-
// Full lists
|
|
36
|
+
// Full ID lists + cursors (round-robin)
|
|
30
37
|
allTopics: [],
|
|
31
38
|
allPosts: [],
|
|
32
39
|
allCategories: [],
|
|
@@ -37,9 +44,6 @@
|
|
|
37
44
|
// throttle per placeholder id
|
|
38
45
|
lastShowById: new Map(),
|
|
39
46
|
|
|
40
|
-
// track placeholders that have been shown at least once in this pageview
|
|
41
|
-
usedOnce: new Set(),
|
|
42
|
-
|
|
43
47
|
// observers / schedulers
|
|
44
48
|
domObs: null,
|
|
45
49
|
io: null,
|
|
@@ -47,22 +51,28 @@
|
|
|
47
51
|
|
|
48
52
|
// hero
|
|
49
53
|
heroDoneForPage: false,
|
|
50
|
-
};
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
// internal DOM changes (to ignore our own mutations)
|
|
56
|
+
internalDomChange: 0,
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// recycle cooldown per kind
|
|
59
|
+
lastRecycleAt: {
|
|
60
|
+
topic: 0,
|
|
61
|
+
categoryTopics: 0,
|
|
62
|
+
categories: 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------- debug ----------
|
|
67
|
+
function debugEnabled() {
|
|
68
|
+
try { return window.localStorage && window.localStorage.getItem('ezoicInfiniteDebug') === '1'; } catch (e) { return false; }
|
|
69
|
+
}
|
|
70
|
+
function debug(...args) {
|
|
71
|
+
if (!debugEnabled()) return;
|
|
72
|
+
try { console.log('[ezoicInfinite]', ...args); } catch (e) {}
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
// ---------- small utils ----------
|
|
65
|
-
|
|
66
76
|
function normalizeBool(v) {
|
|
67
77
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
68
78
|
}
|
|
@@ -89,6 +99,16 @@
|
|
|
89
99
|
return uniqInts(lines);
|
|
90
100
|
}
|
|
91
101
|
|
|
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
|
+
|
|
92
112
|
function getPageKey() {
|
|
93
113
|
try {
|
|
94
114
|
const ax = window.ajaxify;
|
|
@@ -133,8 +153,12 @@
|
|
|
133
153
|
});
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
|
|
156
|
+
function withInternalDomChange(fn) {
|
|
157
|
+
state.internalDomChange++;
|
|
158
|
+
try { return fn(); } finally { state.internalDomChange--; }
|
|
159
|
+
}
|
|
137
160
|
|
|
161
|
+
// ---------- warm-up & patching ----------
|
|
138
162
|
const _warmLinksDone = new Set();
|
|
139
163
|
function warmUpNetwork() {
|
|
140
164
|
try {
|
|
@@ -172,7 +196,7 @@
|
|
|
172
196
|
const orig = ez.showAds;
|
|
173
197
|
|
|
174
198
|
ez.showAds = function (...args) {
|
|
175
|
-
if (
|
|
199
|
+
if (isBlocked()) return;
|
|
176
200
|
|
|
177
201
|
let ids = [];
|
|
178
202
|
if (args.length === 1 && Array.isArray(args[0])) ids = args[0];
|
|
@@ -201,8 +225,7 @@
|
|
|
201
225
|
}
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
// ---------- config &
|
|
205
|
-
|
|
228
|
+
// ---------- config & ids ----------
|
|
206
229
|
async function fetchConfigOnce() {
|
|
207
230
|
if (state.cfg) return state.cfg;
|
|
208
231
|
try {
|
|
@@ -215,7 +238,7 @@
|
|
|
215
238
|
}
|
|
216
239
|
}
|
|
217
240
|
|
|
218
|
-
function
|
|
241
|
+
function initIds(cfg) {
|
|
219
242
|
if (!cfg) return;
|
|
220
243
|
if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
|
|
221
244
|
if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
@@ -223,7 +246,6 @@
|
|
|
223
246
|
}
|
|
224
247
|
|
|
225
248
|
// ---------- insertion primitives ----------
|
|
226
|
-
|
|
227
249
|
function isAdjacentAd(target) {
|
|
228
250
|
if (!target) return false;
|
|
229
251
|
const next = target.nextElementSibling;
|
|
@@ -235,8 +257,9 @@
|
|
|
235
257
|
|
|
236
258
|
function buildWrap(id, kindClass, afterPos) {
|
|
237
259
|
const wrap = document.createElement('div');
|
|
238
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
260
|
+
wrap.className = `${WRAP_CLASS} ${kindClass} is-empty`;
|
|
239
261
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
262
|
+
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
240
263
|
wrap.style.width = '100%';
|
|
241
264
|
|
|
242
265
|
const ph = document.createElement('div');
|
|
@@ -254,26 +277,68 @@
|
|
|
254
277
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
255
278
|
if (!target || !target.insertAdjacentElement) return null;
|
|
256
279
|
if (findWrap(kindClass, afterPos)) return null;
|
|
257
|
-
if (insertingIds.has(id)) return null;
|
|
258
280
|
|
|
259
281
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
260
282
|
if (existingPh && existingPh.isConnected) return null;
|
|
261
283
|
|
|
262
|
-
|
|
263
|
-
try {
|
|
284
|
+
return withInternalDomChange(() => {
|
|
264
285
|
const wrap = buildWrap(id, kindClass, afterPos);
|
|
265
286
|
target.insertAdjacentElement('afterend', wrap);
|
|
266
287
|
return wrap;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
270
335
|
}
|
|
271
336
|
|
|
272
337
|
function pickIdFromAll(allIds, cursorKey) {
|
|
273
338
|
const n = allIds.length;
|
|
274
339
|
if (!n) return null;
|
|
275
340
|
|
|
276
|
-
// Try at most n
|
|
341
|
+
// Try at most n IDs to find one not currently present
|
|
277
342
|
for (let tries = 0; tries < n; tries++) {
|
|
278
343
|
const idx = state[cursorKey] % n;
|
|
279
344
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
@@ -281,93 +346,79 @@
|
|
|
281
346
|
const id = allIds[idx];
|
|
282
347
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
283
348
|
if (ph && ph.isConnected) continue;
|
|
284
|
-
|
|
285
349
|
return id;
|
|
286
350
|
}
|
|
287
351
|
return null;
|
|
288
352
|
}
|
|
289
353
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
294
|
-
if (!wraps.length) return false;
|
|
295
|
-
|
|
296
|
-
// Prefer a wrap far above the viewport
|
|
297
|
-
let victim = null;
|
|
298
|
-
for (const w of wraps) {
|
|
299
|
-
const r = w.getBoundingClientRect();
|
|
300
|
-
if (r.bottom < -2000) { victim = w; break; }
|
|
301
|
-
}
|
|
302
|
-
// Otherwise remove the earliest one in the document
|
|
303
|
-
if (!victim) victim = wraps[0];
|
|
304
|
-
|
|
305
|
-
// Unobserve placeholder if still observed
|
|
354
|
+
function markEmptyLater(id) {
|
|
355
|
+
// Collapse empty blocks if ad never fills
|
|
356
|
+
window.setTimeout(() => {
|
|
306
357
|
try {
|
|
307
|
-
const ph =
|
|
308
|
-
if (ph
|
|
358
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
359
|
+
if (!ph || !ph.isConnected) return;
|
|
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');
|
|
309
365
|
} catch (e) {}
|
|
310
|
-
|
|
311
|
-
victim.remove();
|
|
312
|
-
return true;
|
|
313
|
-
} catch (e) {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
366
|
+
}, 1800);
|
|
316
367
|
}
|
|
317
368
|
|
|
318
369
|
function showAd(id) {
|
|
319
|
-
if (!id ||
|
|
370
|
+
if (!id || isBlocked()) return;
|
|
320
371
|
|
|
321
372
|
const now = Date.now();
|
|
322
373
|
const last = state.lastShowById.get(id) || 0;
|
|
323
374
|
if (now - last < 1500) return; // basic throttle
|
|
324
375
|
|
|
325
|
-
const
|
|
326
|
-
|
|
376
|
+
const doShow = () => {
|
|
377
|
+
if (isBlocked()) return;
|
|
378
|
+
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
379
|
+
if (!ph || !ph.isConnected) return;
|
|
327
380
|
|
|
328
|
-
|
|
381
|
+
state.lastShowById.set(id, Date.now());
|
|
329
382
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
383
|
+
// mark empty by default; remove later if filled
|
|
384
|
+
try {
|
|
385
|
+
const wrap = ph.closest(`.${WRAP_CLASS}`);
|
|
386
|
+
if (wrap) wrap.classList.add('is-empty');
|
|
387
|
+
} catch (e) {}
|
|
333
388
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
338
|
-
ez.destroyPlaceholders(id);
|
|
339
|
-
}
|
|
340
|
-
} catch (e) {}
|
|
341
|
-
ez.showAds(id);
|
|
342
|
-
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
389
|
+
try {
|
|
390
|
+
window.ezstandalone = window.ezstandalone || {};
|
|
391
|
+
const ez = window.ezstandalone;
|
|
345
392
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
// Fast path
|
|
394
|
+
if (typeof ez.showAds === 'function') {
|
|
395
|
+
ez.showAds(id);
|
|
396
|
+
markEmptyLater(id);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Queue once for when Ezoic is ready
|
|
401
|
+
ez.cmd = ez.cmd || [];
|
|
402
|
+
if (!ph.__ezoicQueued) {
|
|
403
|
+
ph.__ezoicQueued = true;
|
|
404
|
+
ez.cmd.push(() => {
|
|
356
405
|
try {
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
406
|
+
if (isBlocked()) return;
|
|
407
|
+
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
408
|
+
if (!el || !el.isConnected) return;
|
|
409
|
+
window.ezstandalone.showAds(id);
|
|
410
|
+
markEmptyLater(id);
|
|
360
411
|
} catch (e) {}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
} catch (e) {}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Defer one frame to reduce "element does not exist" warnings
|
|
418
|
+
window.requestAnimationFrame(doShow);
|
|
367
419
|
}
|
|
368
420
|
|
|
369
421
|
// ---------- preload / above-the-fold ----------
|
|
370
|
-
|
|
371
422
|
function ensurePreloadObserver() {
|
|
372
423
|
if (state.io) return state.io;
|
|
373
424
|
try {
|
|
@@ -401,22 +452,72 @@
|
|
|
401
452
|
} catch (e) {}
|
|
402
453
|
}
|
|
403
454
|
|
|
404
|
-
// ---------- insertion logic ----------
|
|
455
|
+
// ---------- windowed insertion logic ----------
|
|
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
|
+
}
|
|
405
462
|
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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;
|
|
412
480
|
}
|
|
413
|
-
|
|
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
|
+
}
|
|
487
|
+
|
|
488
|
+
function computeTargetsInRange(fromPos, toPos, interval, showFirst) {
|
|
489
|
+
const out = [];
|
|
490
|
+
if (toPos < fromPos) return out;
|
|
491
|
+
|
|
492
|
+
if (showFirst && 1 >= fromPos && 1 <= toPos) out.push(1);
|
|
493
|
+
|
|
494
|
+
const startK = Math.ceil(fromPos / interval) * interval;
|
|
495
|
+
for (let p = startK; p <= toPos; p += interval) out.push(p);
|
|
496
|
+
|
|
497
|
+
return out;
|
|
414
498
|
}
|
|
415
499
|
|
|
416
|
-
function
|
|
500
|
+
function injectWindowed(kind, kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
417
501
|
if (!items.length) return 0;
|
|
418
502
|
|
|
419
|
-
const
|
|
503
|
+
const idsCount = allIds.length;
|
|
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);
|
|
420
521
|
let inserted = 0;
|
|
421
522
|
|
|
422
523
|
for (const afterPos of targets) {
|
|
@@ -428,17 +529,25 @@
|
|
|
428
529
|
if (findWrap(kindClass, afterPos)) continue;
|
|
429
530
|
|
|
430
531
|
let id = pickIdFromAll(allIds, cursorKey);
|
|
532
|
+
|
|
533
|
+
// If no ID is currently free, try a purge (only occasionally) then try again
|
|
431
534
|
if (!id) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
535
|
+
if (!canRecycle(kind)) {
|
|
536
|
+
debug('recycle-skip-cooldown', kindClass);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
const extraKeepFrom = Math.max(1, keepFrom + PURGE_BUFFER_ITEMS);
|
|
540
|
+
const extraKeepTo = Math.min(items.length, keepTo - PURGE_BUFFER_ITEMS);
|
|
541
|
+
const purgedMore = purgeOutsideWindow(kindClass, extraKeepFrom, extraKeepTo);
|
|
542
|
+
debug('recycle-needed', kindClass, { recycled: purgedMore > 0, ids: idsCount });
|
|
543
|
+
|
|
544
|
+
// After a recycle attempt, stop this run and wait for next tick to stabilize DOM
|
|
545
|
+
softBlock(400);
|
|
546
|
+
break;
|
|
436
547
|
}
|
|
437
|
-
|
|
548
|
+
|
|
438
549
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
439
|
-
if (!wrap)
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
550
|
+
if (!wrap) continue;
|
|
442
551
|
|
|
443
552
|
observePlaceholder(id);
|
|
444
553
|
inserted += 1;
|
|
@@ -450,10 +559,9 @@
|
|
|
450
559
|
async function insertHeroAdEarly() {
|
|
451
560
|
if (state.heroDoneForPage) return;
|
|
452
561
|
const cfg = await fetchConfigOnce();
|
|
453
|
-
if (!cfg
|
|
454
|
-
if (cfg.excluded) { dbg('excluded'); return; }
|
|
562
|
+
if (!cfg || cfg.excluded) return;
|
|
455
563
|
|
|
456
|
-
|
|
564
|
+
initIds(cfg);
|
|
457
565
|
|
|
458
566
|
const kind = getKind();
|
|
459
567
|
let items = [];
|
|
@@ -463,31 +571,32 @@
|
|
|
463
571
|
let showFirst = false;
|
|
464
572
|
|
|
465
573
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
574
|
+
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
575
|
+
if (!showFirst) return;
|
|
466
576
|
items = getPostContainers();
|
|
467
577
|
allIds = state.allPosts;
|
|
468
578
|
cursorKey = 'curPosts';
|
|
469
579
|
kindClass = 'ezoic-ad-message';
|
|
470
|
-
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
471
580
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
581
|
+
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
582
|
+
if (!showFirst) return;
|
|
472
583
|
items = getTopicItems();
|
|
473
584
|
allIds = state.allTopics;
|
|
474
585
|
cursorKey = 'curTopics';
|
|
475
586
|
kindClass = 'ezoic-ad-between';
|
|
476
|
-
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
477
587
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
588
|
+
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
589
|
+
if (!showFirst) return;
|
|
478
590
|
items = getCategoryItems();
|
|
479
591
|
allIds = state.allCategories;
|
|
480
592
|
cursorKey = 'curCategories';
|
|
481
593
|
kindClass = 'ezoic-ad-categories';
|
|
482
|
-
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
483
594
|
} else {
|
|
484
595
|
return;
|
|
485
596
|
}
|
|
486
597
|
|
|
487
598
|
if (!items.length) return;
|
|
488
|
-
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
489
599
|
|
|
490
|
-
// Insert after the very first item (above-the-fold)
|
|
491
600
|
const afterPos = 1;
|
|
492
601
|
const el = items[afterPos - 1];
|
|
493
602
|
if (!el || !el.isConnected) return;
|
|
@@ -498,29 +607,27 @@
|
|
|
498
607
|
if (!id) return;
|
|
499
608
|
|
|
500
609
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
501
|
-
if (!wrap)
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
610
|
+
if (!wrap) return;
|
|
504
611
|
|
|
505
612
|
state.heroDoneForPage = true;
|
|
506
613
|
observePlaceholder(id);
|
|
507
614
|
}
|
|
508
615
|
|
|
509
616
|
async function runCore() {
|
|
510
|
-
if (
|
|
617
|
+
if (isBlocked()) return;
|
|
511
618
|
|
|
512
619
|
patchShowAds();
|
|
513
620
|
|
|
514
621
|
const cfg = await fetchConfigOnce();
|
|
515
|
-
if (!cfg
|
|
516
|
-
|
|
517
|
-
initPools(cfg);
|
|
622
|
+
if (!cfg || cfg.excluded) return;
|
|
623
|
+
initIds(cfg);
|
|
518
624
|
|
|
519
625
|
const kind = getKind();
|
|
520
626
|
|
|
521
627
|
if (kind === 'topic') {
|
|
522
628
|
if (normalizeBool(cfg.enableMessageAds)) {
|
|
523
|
-
|
|
629
|
+
injectWindowed(
|
|
630
|
+
'topic',
|
|
524
631
|
'ezoic-ad-message',
|
|
525
632
|
getPostContainers(),
|
|
526
633
|
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
@@ -531,7 +638,8 @@
|
|
|
531
638
|
}
|
|
532
639
|
} else if (kind === 'categoryTopics') {
|
|
533
640
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
534
|
-
|
|
641
|
+
injectWindowed(
|
|
642
|
+
'categoryTopics',
|
|
535
643
|
'ezoic-ad-between',
|
|
536
644
|
getTopicItems(),
|
|
537
645
|
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
@@ -542,7 +650,8 @@
|
|
|
542
650
|
}
|
|
543
651
|
} else if (kind === 'categories') {
|
|
544
652
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
545
|
-
|
|
653
|
+
injectWindowed(
|
|
654
|
+
'categories',
|
|
546
655
|
'ezoic-ad-categories',
|
|
547
656
|
getCategoryItems(),
|
|
548
657
|
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
@@ -566,14 +675,19 @@
|
|
|
566
675
|
}
|
|
567
676
|
|
|
568
677
|
// ---------- observers / lifecycle ----------
|
|
569
|
-
|
|
570
678
|
function cleanup() {
|
|
571
|
-
|
|
679
|
+
softBlock(2000);
|
|
572
680
|
|
|
573
681
|
// remove all wrappers
|
|
574
682
|
try {
|
|
575
|
-
|
|
576
|
-
|
|
683
|
+
withInternalDomChange(() => {
|
|
684
|
+
document.querySelectorAll(`.${WRAP_CLASS}`).forEach((el) => {
|
|
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
|
+
});
|
|
577
691
|
});
|
|
578
692
|
} catch (e) {}
|
|
579
693
|
|
|
@@ -585,17 +699,20 @@
|
|
|
585
699
|
state.curTopics = 0;
|
|
586
700
|
state.curPosts = 0;
|
|
587
701
|
state.curCategories = 0;
|
|
702
|
+
|
|
588
703
|
state.lastShowById.clear();
|
|
589
|
-
try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
|
|
590
704
|
state.heroDoneForPage = false;
|
|
591
|
-
|
|
592
|
-
|
|
705
|
+
state.lastRecycleAt.topic = 0;
|
|
706
|
+
state.lastRecycleAt.categoryTopics = 0;
|
|
707
|
+
state.lastRecycleAt.categories = 0;
|
|
593
708
|
}
|
|
594
709
|
|
|
595
710
|
function ensureDomObserver() {
|
|
596
711
|
if (state.domObs) return;
|
|
597
712
|
state.domObs = new MutationObserver(() => {
|
|
598
|
-
if (
|
|
713
|
+
if (state.internalDomChange > 0) return;
|
|
714
|
+
if (isBlocked()) return;
|
|
715
|
+
scheduleRun();
|
|
599
716
|
});
|
|
600
717
|
try {
|
|
601
718
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
@@ -613,7 +730,7 @@
|
|
|
613
730
|
|
|
614
731
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
615
732
|
state.pageKey = getPageKey();
|
|
616
|
-
|
|
733
|
+
softBlock(500);
|
|
617
734
|
|
|
618
735
|
warmUpNetwork();
|
|
619
736
|
patchShowAds();
|
|
@@ -629,7 +746,7 @@
|
|
|
629
746
|
|
|
630
747
|
// Infinite scroll / partial updates
|
|
631
748
|
$(window).on('action:posts.loaded.ezoicInfinite action:topics.loaded.ezoicInfinite action:category.loaded.ezoicInfinite action:topic.loaded.ezoicInfinite', () => {
|
|
632
|
-
if (
|
|
749
|
+
if (isBlocked()) return;
|
|
633
750
|
scheduleRun();
|
|
634
751
|
});
|
|
635
752
|
}
|
|
@@ -641,13 +758,12 @@
|
|
|
641
758
|
ticking = true;
|
|
642
759
|
window.requestAnimationFrame(() => {
|
|
643
760
|
ticking = false;
|
|
644
|
-
if (!
|
|
761
|
+
if (!isBlocked()) scheduleRun();
|
|
645
762
|
});
|
|
646
763
|
}, { passive: true });
|
|
647
764
|
}
|
|
648
765
|
|
|
649
766
|
// ---------- boot ----------
|
|
650
|
-
|
|
651
767
|
state.pageKey = getPageKey();
|
|
652
768
|
warmUpNetwork();
|
|
653
769
|
patchShowAds();
|
|
@@ -658,7 +774,7 @@
|
|
|
658
774
|
bindScroll();
|
|
659
775
|
|
|
660
776
|
// First paint: try hero + run
|
|
661
|
-
|
|
777
|
+
softBlock(300);
|
|
662
778
|
insertHeroAdEarly().catch(() => {});
|
|
663
779
|
scheduleRun();
|
|
664
|
-
})();
|
|
780
|
+
})();
|
package/public/style.css
CHANGED
|
@@ -19,3 +19,21 @@
|
|
|
19
19
|
margin: 0 !important;
|
|
20
20
|
padding: 0 !important;
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/* Collapse empty ad blocks (prevents "holes" when an ad doesn't fill or gets destroyed) */
|
|
25
|
+
.ezoic-ad.is-empty {
|
|
26
|
+
display: none !important;
|
|
27
|
+
margin: 0 !important;
|
|
28
|
+
padding: 0 !important;
|
|
29
|
+
height: 0 !important;
|
|
30
|
+
min-height: 0 !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ezoic-ad {
|
|
34
|
+
min-height: 0 !important;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
38
|
+
min-height: 0 !important;
|
|
39
|
+
}
|