nodebb-plugin-ezoic-infinite 1.5.27 → 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 +232 -166
- package/public/style.css +18 -0
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -13,23 +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
|
-
// 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
|
-
|
|
28
32
|
const state = {
|
|
29
33
|
pageKey: null,
|
|
30
34
|
cfg: null,
|
|
31
35
|
|
|
32
|
-
// Full lists
|
|
36
|
+
// Full ID lists + cursors (round-robin)
|
|
33
37
|
allTopics: [],
|
|
34
38
|
allPosts: [],
|
|
35
39
|
allCategories: [],
|
|
@@ -39,11 +43,6 @@
|
|
|
39
43
|
|
|
40
44
|
// throttle per placeholder id
|
|
41
45
|
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(),
|
|
47
46
|
|
|
48
47
|
// observers / schedulers
|
|
49
48
|
domObs: null,
|
|
@@ -52,22 +51,28 @@
|
|
|
52
51
|
|
|
53
52
|
// hero
|
|
54
53
|
heroDoneForPage: false,
|
|
55
|
-
};
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
// internal DOM changes (to ignore our own mutations)
|
|
56
|
+
internalDomChange: 0,
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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) {}
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
// ---------- small utils ----------
|
|
70
|
-
|
|
71
76
|
function normalizeBool(v) {
|
|
72
77
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
73
78
|
}
|
|
@@ -94,6 +99,16 @@
|
|
|
94
99
|
return uniqInts(lines);
|
|
95
100
|
}
|
|
96
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
|
+
|
|
97
112
|
function getPageKey() {
|
|
98
113
|
try {
|
|
99
114
|
const ax = window.ajaxify;
|
|
@@ -138,8 +153,12 @@
|
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
|
|
141
|
-
|
|
156
|
+
function withInternalDomChange(fn) {
|
|
157
|
+
state.internalDomChange++;
|
|
158
|
+
try { return fn(); } finally { state.internalDomChange--; }
|
|
159
|
+
}
|
|
142
160
|
|
|
161
|
+
// ---------- warm-up & patching ----------
|
|
143
162
|
const _warmLinksDone = new Set();
|
|
144
163
|
function warmUpNetwork() {
|
|
145
164
|
try {
|
|
@@ -206,29 +225,7 @@
|
|
|
206
225
|
}
|
|
207
226
|
}
|
|
208
227
|
|
|
209
|
-
|
|
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
|
-
|
|
228
|
+
// ---------- config & ids ----------
|
|
232
229
|
async function fetchConfigOnce() {
|
|
233
230
|
if (state.cfg) return state.cfg;
|
|
234
231
|
try {
|
|
@@ -241,7 +238,7 @@ function withInternalDomChange(fn) {
|
|
|
241
238
|
}
|
|
242
239
|
}
|
|
243
240
|
|
|
244
|
-
function
|
|
241
|
+
function initIds(cfg) {
|
|
245
242
|
if (!cfg) return;
|
|
246
243
|
if (state.allTopics.length === 0) state.allTopics = parsePool(cfg.placeholderIds);
|
|
247
244
|
if (state.allPosts.length === 0) state.allPosts = parsePool(cfg.messagePlaceholderIds);
|
|
@@ -249,7 +246,6 @@ function withInternalDomChange(fn) {
|
|
|
249
246
|
}
|
|
250
247
|
|
|
251
248
|
// ---------- insertion primitives ----------
|
|
252
|
-
|
|
253
249
|
function isAdjacentAd(target) {
|
|
254
250
|
if (!target) return false;
|
|
255
251
|
const next = target.nextElementSibling;
|
|
@@ -261,8 +257,9 @@ function withInternalDomChange(fn) {
|
|
|
261
257
|
|
|
262
258
|
function buildWrap(id, kindClass, afterPos) {
|
|
263
259
|
const wrap = document.createElement('div');
|
|
264
|
-
wrap.className = `${WRAP_CLASS} ${kindClass}`;
|
|
260
|
+
wrap.className = `${WRAP_CLASS} ${kindClass} is-empty`;
|
|
265
261
|
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
262
|
+
wrap.setAttribute('data-ezoic-wrapid', String(id));
|
|
266
263
|
wrap.style.width = '100%';
|
|
267
264
|
|
|
268
265
|
const ph = document.createElement('div');
|
|
@@ -280,26 +277,68 @@ function withInternalDomChange(fn) {
|
|
|
280
277
|
function insertAfter(target, id, kindClass, afterPos) {
|
|
281
278
|
if (!target || !target.insertAdjacentElement) return null;
|
|
282
279
|
if (findWrap(kindClass, afterPos)) return null;
|
|
283
|
-
if (insertingIds.has(id)) return null;
|
|
284
280
|
|
|
285
281
|
const existingPh = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
286
282
|
if (existingPh && existingPh.isConnected) return null;
|
|
287
283
|
|
|
288
|
-
|
|
289
|
-
try {
|
|
284
|
+
return withInternalDomChange(() => {
|
|
290
285
|
const wrap = buildWrap(id, kindClass, afterPos);
|
|
291
286
|
target.insertAdjacentElement('afterend', wrap);
|
|
292
287
|
return wrap;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
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;
|
|
296
335
|
}
|
|
297
336
|
|
|
298
337
|
function pickIdFromAll(allIds, cursorKey) {
|
|
299
338
|
const n = allIds.length;
|
|
300
339
|
if (!n) return null;
|
|
301
340
|
|
|
302
|
-
// Try at most n
|
|
341
|
+
// Try at most n IDs to find one not currently present
|
|
303
342
|
for (let tries = 0; tries < n; tries++) {
|
|
304
343
|
const idx = state[cursorKey] % n;
|
|
305
344
|
state[cursorKey] = (state[cursorKey] + 1) % n;
|
|
@@ -307,38 +346,24 @@ function withInternalDomChange(fn) {
|
|
|
307
346
|
const id = allIds[idx];
|
|
308
347
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
309
348
|
if (ph && ph.isConnected) continue;
|
|
310
|
-
|
|
311
349
|
return id;
|
|
312
350
|
}
|
|
313
351
|
return null;
|
|
314
352
|
}
|
|
315
353
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const wraps = Array.from(document.querySelectorAll(`.${WRAP_CLASS}.${kindClass}`));
|
|
320
|
-
if (!wraps.length) return false;
|
|
321
|
-
|
|
322
|
-
// Prefer a wrap far above the viewport
|
|
323
|
-
let victim = null;
|
|
324
|
-
for (const w of wraps) {
|
|
325
|
-
const r = w.getBoundingClientRect();
|
|
326
|
-
if (r.bottom < -2000) { victim = w; break; }
|
|
327
|
-
}
|
|
328
|
-
// Otherwise remove the earliest one in the document
|
|
329
|
-
if (!victim) victim = wraps[0];
|
|
330
|
-
|
|
331
|
-
// Unobserve placeholder if still observed
|
|
354
|
+
function markEmptyLater(id) {
|
|
355
|
+
// Collapse empty blocks if ad never fills
|
|
356
|
+
window.setTimeout(() => {
|
|
332
357
|
try {
|
|
333
|
-
const ph =
|
|
334
|
-
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');
|
|
335
365
|
} catch (e) {}
|
|
336
|
-
|
|
337
|
-
victim.remove();
|
|
338
|
-
return true;
|
|
339
|
-
} catch (e) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
366
|
+
}, 1800);
|
|
342
367
|
}
|
|
343
368
|
|
|
344
369
|
function showAd(id) {
|
|
@@ -348,36 +373,27 @@ function withInternalDomChange(fn) {
|
|
|
348
373
|
const last = state.lastShowById.get(id) || 0;
|
|
349
374
|
if (now - last < 1500) return; // basic throttle
|
|
350
375
|
|
|
351
|
-
|
|
352
|
-
requestAnimationFrame(() => {
|
|
376
|
+
const doShow = () => {
|
|
353
377
|
if (isBlocked()) return;
|
|
354
|
-
|
|
355
378
|
const ph = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
356
379
|
if (!ph || !ph.isConnected) return;
|
|
357
380
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
381
|
+
state.lastShowById.set(id, Date.now());
|
|
382
|
+
|
|
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) {}
|
|
362
388
|
|
|
363
389
|
try {
|
|
364
390
|
window.ezstandalone = window.ezstandalone || {};
|
|
365
391
|
const ez = window.ezstandalone;
|
|
366
392
|
|
|
367
|
-
const doShow = () => {
|
|
368
|
-
try {
|
|
369
|
-
if (state.usedOnce && state.usedOnce.has(id) && typeof ez.destroyPlaceholders === 'function') {
|
|
370
|
-
// Avoid Ezoic caching state for reused placeholders
|
|
371
|
-
ez.destroyPlaceholders(id);
|
|
372
|
-
}
|
|
373
|
-
} catch (e) {}
|
|
374
|
-
try { ez.showAds(id); } catch (e) {}
|
|
375
|
-
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
393
|
// Fast path
|
|
379
394
|
if (typeof ez.showAds === 'function') {
|
|
380
|
-
|
|
395
|
+
ez.showAds(id);
|
|
396
|
+
markEmptyLater(id);
|
|
381
397
|
return;
|
|
382
398
|
}
|
|
383
399
|
|
|
@@ -390,25 +406,19 @@ function withInternalDomChange(fn) {
|
|
|
390
406
|
if (isBlocked()) return;
|
|
391
407
|
const el = document.getElementById(`${PLACEHOLDER_PREFIX}${id}`);
|
|
392
408
|
if (!el || !el.isConnected) return;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
if (state.usedOnce && state.usedOnce.has(id) && typeof ez2.destroyPlaceholders === 'function') {
|
|
397
|
-
ez2.destroyPlaceholders(id);
|
|
398
|
-
}
|
|
399
|
-
} catch (e) {}
|
|
400
|
-
try { ez2.showAds(id); } catch (e) {}
|
|
401
|
-
try { state.usedOnce && state.usedOnce.add(id); } catch (e) {}
|
|
409
|
+
window.ezstandalone.showAds(id);
|
|
410
|
+
markEmptyLater(id);
|
|
402
411
|
} catch (e) {}
|
|
403
412
|
});
|
|
404
413
|
}
|
|
405
414
|
} catch (e) {}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
415
|
+
};
|
|
408
416
|
|
|
417
|
+
// Defer one frame to reduce "element does not exist" warnings
|
|
418
|
+
window.requestAnimationFrame(doShow);
|
|
419
|
+
}
|
|
409
420
|
|
|
410
421
|
// ---------- preload / above-the-fold ----------
|
|
411
|
-
|
|
412
422
|
function ensurePreloadObserver() {
|
|
413
423
|
if (state.io) return state.io;
|
|
414
424
|
try {
|
|
@@ -442,22 +452,72 @@ function withInternalDomChange(fn) {
|
|
|
442
452
|
} catch (e) {}
|
|
443
453
|
}
|
|
444
454
|
|
|
445
|
-
// ---------- 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
|
+
}
|
|
446
462
|
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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;
|
|
453
480
|
}
|
|
454
|
-
|
|
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;
|
|
455
498
|
}
|
|
456
499
|
|
|
457
|
-
function
|
|
500
|
+
function injectWindowed(kind, kindClass, items, interval, showFirst, allIds, cursorKey) {
|
|
458
501
|
if (!items.length) return 0;
|
|
459
502
|
|
|
460
|
-
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);
|
|
461
521
|
let inserted = 0;
|
|
462
522
|
|
|
463
523
|
for (const afterPos of targets) {
|
|
@@ -469,25 +529,25 @@ function withInternalDomChange(fn) {
|
|
|
469
529
|
if (findWrap(kindClass, afterPos)) continue;
|
|
470
530
|
|
|
471
531
|
let id = pickIdFromAll(allIds, cursorKey);
|
|
532
|
+
|
|
533
|
+
// If no ID is currently free, try a purge (only occasionally) then try again
|
|
472
534
|
if (!id) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (!canRecycle(kindKeyFromClass(kindClass))) {
|
|
476
|
-
dbg('recycle-skip-cooldown', kindClass);
|
|
535
|
+
if (!canRecycle(kind)) {
|
|
536
|
+
debug('recycle-skip-cooldown', kindClass);
|
|
477
537
|
break;
|
|
478
538
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
//
|
|
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);
|
|
485
546
|
break;
|
|
486
547
|
}
|
|
548
|
+
|
|
487
549
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
488
|
-
if (!wrap)
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
550
|
+
if (!wrap) continue;
|
|
491
551
|
|
|
492
552
|
observePlaceholder(id);
|
|
493
553
|
inserted += 1;
|
|
@@ -499,10 +559,9 @@ function withInternalDomChange(fn) {
|
|
|
499
559
|
async function insertHeroAdEarly() {
|
|
500
560
|
if (state.heroDoneForPage) return;
|
|
501
561
|
const cfg = await fetchConfigOnce();
|
|
502
|
-
if (!cfg
|
|
503
|
-
if (cfg.excluded) { dbg('excluded'); return; }
|
|
562
|
+
if (!cfg || cfg.excluded) return;
|
|
504
563
|
|
|
505
|
-
|
|
564
|
+
initIds(cfg);
|
|
506
565
|
|
|
507
566
|
const kind = getKind();
|
|
508
567
|
let items = [];
|
|
@@ -512,31 +571,32 @@ function withInternalDomChange(fn) {
|
|
|
512
571
|
let showFirst = false;
|
|
513
572
|
|
|
514
573
|
if (kind === 'topic' && normalizeBool(cfg.enableMessageAds)) {
|
|
574
|
+
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
575
|
+
if (!showFirst) return;
|
|
515
576
|
items = getPostContainers();
|
|
516
577
|
allIds = state.allPosts;
|
|
517
578
|
cursorKey = 'curPosts';
|
|
518
579
|
kindClass = 'ezoic-ad-message';
|
|
519
|
-
showFirst = normalizeBool(cfg.showFirstMessageAd);
|
|
520
580
|
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
581
|
+
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
582
|
+
if (!showFirst) return;
|
|
521
583
|
items = getTopicItems();
|
|
522
584
|
allIds = state.allTopics;
|
|
523
585
|
cursorKey = 'curTopics';
|
|
524
586
|
kindClass = 'ezoic-ad-between';
|
|
525
|
-
showFirst = normalizeBool(cfg.showFirstTopicAd);
|
|
526
587
|
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
588
|
+
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
589
|
+
if (!showFirst) return;
|
|
527
590
|
items = getCategoryItems();
|
|
528
591
|
allIds = state.allCategories;
|
|
529
592
|
cursorKey = 'curCategories';
|
|
530
593
|
kindClass = 'ezoic-ad-categories';
|
|
531
|
-
showFirst = normalizeBool(cfg.showFirstCategoryAd);
|
|
532
594
|
} else {
|
|
533
595
|
return;
|
|
534
596
|
}
|
|
535
597
|
|
|
536
598
|
if (!items.length) return;
|
|
537
|
-
if (!showFirst) { state.heroDoneForPage = true; return; }
|
|
538
599
|
|
|
539
|
-
// Insert after the very first item (above-the-fold)
|
|
540
600
|
const afterPos = 1;
|
|
541
601
|
const el = items[afterPos - 1];
|
|
542
602
|
if (!el || !el.isConnected) return;
|
|
@@ -547,29 +607,27 @@ function withInternalDomChange(fn) {
|
|
|
547
607
|
if (!id) return;
|
|
548
608
|
|
|
549
609
|
const wrap = insertAfter(el, id, kindClass, afterPos);
|
|
550
|
-
if (!wrap)
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
610
|
+
if (!wrap) return;
|
|
553
611
|
|
|
554
612
|
state.heroDoneForPage = true;
|
|
555
613
|
observePlaceholder(id);
|
|
556
614
|
}
|
|
557
615
|
|
|
558
616
|
async function runCore() {
|
|
559
|
-
if (isBlocked())
|
|
617
|
+
if (isBlocked()) return;
|
|
560
618
|
|
|
561
619
|
patchShowAds();
|
|
562
620
|
|
|
563
621
|
const cfg = await fetchConfigOnce();
|
|
564
|
-
if (!cfg
|
|
565
|
-
|
|
566
|
-
initPools(cfg);
|
|
622
|
+
if (!cfg || cfg.excluded) return;
|
|
623
|
+
initIds(cfg);
|
|
567
624
|
|
|
568
625
|
const kind = getKind();
|
|
569
626
|
|
|
570
627
|
if (kind === 'topic') {
|
|
571
628
|
if (normalizeBool(cfg.enableMessageAds)) {
|
|
572
|
-
|
|
629
|
+
injectWindowed(
|
|
630
|
+
'topic',
|
|
573
631
|
'ezoic-ad-message',
|
|
574
632
|
getPostContainers(),
|
|
575
633
|
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
@@ -580,7 +638,8 @@ function withInternalDomChange(fn) {
|
|
|
580
638
|
}
|
|
581
639
|
} else if (kind === 'categoryTopics') {
|
|
582
640
|
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
583
|
-
|
|
641
|
+
injectWindowed(
|
|
642
|
+
'categoryTopics',
|
|
584
643
|
'ezoic-ad-between',
|
|
585
644
|
getTopicItems(),
|
|
586
645
|
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
@@ -591,7 +650,8 @@ function withInternalDomChange(fn) {
|
|
|
591
650
|
}
|
|
592
651
|
} else if (kind === 'categories') {
|
|
593
652
|
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
594
|
-
|
|
653
|
+
injectWindowed(
|
|
654
|
+
'categories',
|
|
595
655
|
'ezoic-ad-categories',
|
|
596
656
|
getCategoryItems(),
|
|
597
657
|
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
@@ -615,14 +675,19 @@ function withInternalDomChange(fn) {
|
|
|
615
675
|
}
|
|
616
676
|
|
|
617
677
|
// ---------- observers / lifecycle ----------
|
|
618
|
-
|
|
619
678
|
function cleanup() {
|
|
620
|
-
|
|
679
|
+
softBlock(2000);
|
|
621
680
|
|
|
622
681
|
// remove all wrappers
|
|
623
682
|
try {
|
|
624
|
-
|
|
625
|
-
|
|
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
|
+
});
|
|
626
691
|
});
|
|
627
692
|
} catch (e) {}
|
|
628
693
|
|
|
@@ -634,18 +699,20 @@ function withInternalDomChange(fn) {
|
|
|
634
699
|
state.curTopics = 0;
|
|
635
700
|
state.curPosts = 0;
|
|
636
701
|
state.curCategories = 0;
|
|
702
|
+
|
|
637
703
|
state.lastShowById.clear();
|
|
638
|
-
try { state.usedOnce && state.usedOnce.clear(); } catch (e) {}
|
|
639
704
|
state.heroDoneForPage = false;
|
|
640
|
-
|
|
641
|
-
|
|
705
|
+
state.lastRecycleAt.topic = 0;
|
|
706
|
+
state.lastRecycleAt.categoryTopics = 0;
|
|
707
|
+
state.lastRecycleAt.categories = 0;
|
|
642
708
|
}
|
|
643
709
|
|
|
644
710
|
function ensureDomObserver() {
|
|
645
711
|
if (state.domObs) return;
|
|
646
712
|
state.domObs = new MutationObserver(() => {
|
|
647
713
|
if (state.internalDomChange > 0) return;
|
|
648
|
-
if (
|
|
714
|
+
if (isBlocked()) return;
|
|
715
|
+
scheduleRun();
|
|
649
716
|
});
|
|
650
717
|
try {
|
|
651
718
|
state.domObs.observe(document.body, { childList: true, subtree: true });
|
|
@@ -663,7 +730,7 @@ function withInternalDomChange(fn) {
|
|
|
663
730
|
|
|
664
731
|
$(window).on('action:ajaxify.end.ezoicInfinite', () => {
|
|
665
732
|
state.pageKey = getPageKey();
|
|
666
|
-
|
|
733
|
+
softBlock(500);
|
|
667
734
|
|
|
668
735
|
warmUpNetwork();
|
|
669
736
|
patchShowAds();
|
|
@@ -697,7 +764,6 @@ function withInternalDomChange(fn) {
|
|
|
697
764
|
}
|
|
698
765
|
|
|
699
766
|
// ---------- boot ----------
|
|
700
|
-
|
|
701
767
|
state.pageKey = getPageKey();
|
|
702
768
|
warmUpNetwork();
|
|
703
769
|
patchShowAds();
|
|
@@ -708,7 +774,7 @@ function withInternalDomChange(fn) {
|
|
|
708
774
|
bindScroll();
|
|
709
775
|
|
|
710
776
|
// First paint: try hero + run
|
|
711
|
-
|
|
777
|
+
softBlock(300);
|
|
712
778
|
insertHeroAdEarly().catch(() => {});
|
|
713
779
|
scheduleRun();
|
|
714
|
-
})();
|
|
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
|
+
}
|