nodebb-plugin-ezoic-infinite 1.5.20 → 1.5.21
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/library.js +44 -10
- package/package.json +1 -1
- package/plugin.json +4 -0
- package/public/admin.js +2 -3
- package/public/client.js +676 -196
- package/public/style.css +15 -5
package/library.js
CHANGED
|
@@ -9,7 +9,11 @@ const plugin = {};
|
|
|
9
9
|
|
|
10
10
|
function normalizeExcludedGroups(value) {
|
|
11
11
|
if (!value) return [];
|
|
12
|
-
|
|
12
|
+
// NodeBB settings may return arrays, strings, or objects like {0:'a',1:'b'}
|
|
13
|
+
if (Array.isArray(value)) return value.map(String).map(s => s.trim()).filter(Boolean);
|
|
14
|
+
if (typeof value === 'object') {
|
|
15
|
+
return Object.values(value).map(String).map(s => s.trim()).filter(Boolean);
|
|
16
|
+
}
|
|
13
17
|
return String(value).split(',').map(s => s.trim()).filter(Boolean);
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -27,13 +31,21 @@ async function getAllGroups() {
|
|
|
27
31
|
}
|
|
28
32
|
const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
|
|
29
33
|
const data = await groups.getGroupsData(filtered);
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// Filter out nulls (groups deleted between the sorted-set read and getGroupsData)
|
|
35
|
+
const valid = data.filter(g => g && g.name);
|
|
36
|
+
valid.sort((a, b) => String(a.name).localeCompare(String(b.name), undefined, { sensitivity: 'base' }));
|
|
37
|
+
return valid;
|
|
33
38
|
}
|
|
39
|
+
let _settingsCache = null;
|
|
40
|
+
let _settingsCacheAt = 0;
|
|
41
|
+
const SETTINGS_TTL = 30000; // 30s
|
|
42
|
+
|
|
34
43
|
async function getSettings() {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
if (_settingsCache && (now - _settingsCacheAt) < SETTINGS_TTL) return _settingsCache;
|
|
35
46
|
const s = await meta.settings.get(SETTINGS_KEY);
|
|
36
|
-
|
|
47
|
+
_settingsCacheAt = Date.now();
|
|
48
|
+
_settingsCache = {
|
|
37
49
|
// Between-post ads (simple blocks) in category topic list
|
|
38
50
|
enableBetweenAds: parseBool(s.enableBetweenAds, true),
|
|
39
51
|
showFirstTopicAd: parseBool(s.showFirstTopicAd, false),
|
|
@@ -54,14 +66,32 @@ async function getSettings() {
|
|
|
54
66
|
|
|
55
67
|
excludedGroups: normalizeExcludedGroups(s.excludedGroups),
|
|
56
68
|
};
|
|
69
|
+
return _settingsCache;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
async function isUserExcluded(uid, excludedGroups) {
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
73
|
+
const list = (excludedGroups || []).map(g => String(g).toLowerCase());
|
|
74
|
+
const id = Number(uid) || 0;
|
|
75
|
+
|
|
76
|
+
if (!list.length) return false;
|
|
77
|
+
|
|
78
|
+
// Guests (uid=0) are not in groups.getUserGroups; treat explicitly if configured
|
|
79
|
+
if (id === 0) {
|
|
80
|
+
return list.includes('guests') || list.includes('guest');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const userGroups = await groups.getUserGroups([id]);
|
|
84
|
+
const names = (userGroups[0] || []).map(g => (g && g.name) ? g.name : String(g));
|
|
85
|
+
return names.some(name => list.includes(String(name).toLowerCase()));
|
|
63
86
|
}
|
|
64
87
|
|
|
88
|
+
plugin.onSettingsSet = function (data) {
|
|
89
|
+
// Invalider le cache dès que les settings de ce plugin sont sauvegardés via l'ACP
|
|
90
|
+
if (data && data.hash === SETTINGS_KEY) {
|
|
91
|
+
_settingsCache = null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
65
95
|
plugin.addAdminNavigation = async (header) => {
|
|
66
96
|
header.plugins = header.plugins || [];
|
|
67
97
|
header.plugins.push({
|
|
@@ -89,9 +119,13 @@ plugin.init = async ({ router, middleware }) => {
|
|
|
89
119
|
router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
|
|
90
120
|
router.get('/api/admin/plugins/ezoic-infinite', render);
|
|
91
121
|
|
|
92
|
-
router.get('/api/plugins/ezoic-infinite/config',
|
|
122
|
+
router.get('/api/plugins/ezoic-infinite/config', async (req, res) => {
|
|
93
123
|
const settings = await getSettings();
|
|
94
|
-
const
|
|
124
|
+
const uid = (typeof req.uid !== 'undefined' && req.uid !== null) ? req.uid
|
|
125
|
+
: (req.user && req.user.uid) ? req.user.uid
|
|
126
|
+
: (res.locals && res.locals.uid) ? res.locals.uid
|
|
127
|
+
: 0;
|
|
128
|
+
const excluded = await isUserExcluded(uid, settings.excludedGroups);
|
|
95
129
|
|
|
96
130
|
res.json({
|
|
97
131
|
excluded,
|
package/package.json
CHANGED
package/plugin.json
CHANGED
package/public/admin.js
CHANGED
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
e.preventDefault();
|
|
14
14
|
|
|
15
15
|
Settings.save('ezoic-infinite', $form, function () {
|
|
16
|
-
// Toast vert (NodeBB core)
|
|
17
16
|
if (alerts && typeof alerts.success === 'function') {
|
|
18
|
-
alerts.success('
|
|
17
|
+
alerts.success('[[admin/settings:saved]]');
|
|
19
18
|
} else if (window.app && typeof window.app.alertSuccess === 'function') {
|
|
20
|
-
window.app.alertSuccess('
|
|
19
|
+
window.app.alertSuccess('[[admin/settings:saved]]');
|
|
21
20
|
}
|
|
22
21
|
});
|
|
23
22
|
});
|
package/public/client.js
CHANGED
|
@@ -1,45 +1,93 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
var $ = (typeof window.jQuery === 'function') ? window.jQuery : null;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
var SELECTORS = {
|
|
7
7
|
topicItem: 'li[component="category/topic"]',
|
|
8
8
|
postItem: '[component="post"][data-pid]',
|
|
9
|
-
categoryItem: 'li[component="categories/category"]'
|
|
9
|
+
categoryItem: 'li[component="categories/category"]'
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const MAX_INSERTS_PER_RUN = 3;
|
|
12
|
+
var WRAP_CLASS = 'ezoic-ad';
|
|
13
|
+
var PLACEHOLDER_PREFIX = 'ezoic-pub-ad-placeholder-';
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
// Hard limits to avoid runaway insertion during mutations / infinite scroll.
|
|
16
|
+
var MAX_INSERTS_PER_RUN = 3;
|
|
17
|
+
|
|
18
|
+
// Placeholders that have been "defined" (filled) at least once in this browser session.
|
|
19
|
+
// This survives ajaxify navigations, which is important for safe recycle/destroy logic.
|
|
20
|
+
var sessionDefinedIds = new Set();
|
|
21
|
+
|
|
22
|
+
// Prevent re-entrant insertion of the same id while Ezoic is processing it.
|
|
23
|
+
var insertingIds = new Set();
|
|
24
|
+
|
|
25
|
+
var state = {
|
|
17
26
|
pageKey: null,
|
|
27
|
+
|
|
18
28
|
cfg: null,
|
|
19
29
|
cfgPromise: null,
|
|
30
|
+
|
|
20
31
|
poolTopics: [],
|
|
21
32
|
poolPosts: [],
|
|
22
33
|
poolCategories: [],
|
|
34
|
+
|
|
23
35
|
usedTopics: new Set(),
|
|
24
36
|
usedPosts: new Set(),
|
|
25
37
|
usedCategories: new Set(),
|
|
38
|
+
|
|
39
|
+
// Track wrappers that are still in the DOM to recycle ids once they are far above viewport.
|
|
40
|
+
liveTopics: [],
|
|
41
|
+
livePosts: [],
|
|
42
|
+
liveCategories: [],
|
|
43
|
+
|
|
44
|
+
// Throttle showAds calls per id.
|
|
26
45
|
lastShowById: new Map(),
|
|
27
|
-
|
|
46
|
+
|
|
47
|
+
// Ids for which we scheduled/attempted showAds and should not schedule again immediately.
|
|
48
|
+
pendingById: new Set(),
|
|
49
|
+
|
|
50
|
+
// Timeouts created by this script (so we can cancel on ajaxify.start).
|
|
51
|
+
activeTimeouts: new Set(),
|
|
52
|
+
|
|
53
|
+
// Run scheduling / mutation observer.
|
|
28
54
|
scheduled: false,
|
|
29
55
|
timer: null,
|
|
30
56
|
obs: null,
|
|
31
|
-
};
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
// Scroll throttling.
|
|
59
|
+
lastScrollRun: 0,
|
|
60
|
+
|
|
61
|
+
// Navigation safety gate: we only insert after ajaxify.end settles.
|
|
62
|
+
canShowAds: false,
|
|
63
|
+
|
|
64
|
+
// Retry counters.
|
|
65
|
+
poolWaitAttempts: 0,
|
|
66
|
+
awaitItemsAttempts: 0
|
|
67
|
+
};
|
|
34
68
|
|
|
35
69
|
function normalizeBool(v) {
|
|
36
70
|
return v === true || v === 'true' || v === 1 || v === '1' || v === 'on';
|
|
37
71
|
}
|
|
38
72
|
|
|
73
|
+
function setTimeoutTracked(fn, ms) {
|
|
74
|
+
var id = setTimeout(fn, ms);
|
|
75
|
+
state.activeTimeouts.add(id);
|
|
76
|
+
return id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function clearAllTrackedTimeouts() {
|
|
80
|
+
state.activeTimeouts.forEach(function (id) {
|
|
81
|
+
try { clearTimeout(id); } catch (e) {}
|
|
82
|
+
});
|
|
83
|
+
state.activeTimeouts.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
39
86
|
function uniqInts(lines) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
87
|
+
var out = [];
|
|
88
|
+
var seen = new Set();
|
|
89
|
+
for (var i = 0; i < lines.length; i++) {
|
|
90
|
+
var n = parseInt(lines[i], 10);
|
|
43
91
|
if (Number.isFinite(n) && n > 0 && !seen.has(n)) {
|
|
44
92
|
seen.add(n);
|
|
45
93
|
out.push(n);
|
|
@@ -50,26 +98,34 @@
|
|
|
50
98
|
|
|
51
99
|
function parsePool(raw) {
|
|
52
100
|
if (!raw) return [];
|
|
53
|
-
|
|
101
|
+
var lines = String(raw)
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.map(function (s) { return s.trim(); })
|
|
104
|
+
.filter(Boolean);
|
|
54
105
|
return uniqInts(lines);
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
function getPageKey() {
|
|
58
109
|
try {
|
|
59
|
-
|
|
110
|
+
var ax = window.ajaxify;
|
|
60
111
|
if (ax && ax.data) {
|
|
61
|
-
if (ax.data.tid) return
|
|
62
|
-
if (ax.data.cid) return
|
|
112
|
+
if (ax.data.tid) return 'topic:' + ax.data.tid;
|
|
113
|
+
if (ax.data.cid) return 'cid:' + ax.data.cid + ':' + window.location.pathname;
|
|
63
114
|
}
|
|
64
115
|
} catch (e) {}
|
|
65
116
|
return window.location.pathname;
|
|
66
117
|
}
|
|
67
118
|
|
|
68
119
|
function getKind() {
|
|
69
|
-
|
|
120
|
+
var p = window.location.pathname || '';
|
|
70
121
|
if (/^\/topic\//.test(p)) return 'topic';
|
|
71
122
|
if (/^\/category\//.test(p)) return 'categoryTopics';
|
|
72
123
|
if (p === '/' || /^\/categories/.test(p)) return 'categories';
|
|
124
|
+
|
|
125
|
+
// Fallback by DOM.
|
|
126
|
+
if (document.querySelector(SELECTORS.categoryItem)) return 'categories';
|
|
127
|
+
if (document.querySelector(SELECTORS.postItem)) return 'topic';
|
|
128
|
+
if (document.querySelector(SELECTORS.topicItem)) return 'categoryTopics';
|
|
73
129
|
return 'other';
|
|
74
130
|
}
|
|
75
131
|
|
|
@@ -77,295 +133,719 @@
|
|
|
77
133
|
return Array.from(document.querySelectorAll(SELECTORS.topicItem));
|
|
78
134
|
}
|
|
79
135
|
|
|
80
|
-
function getPostContainers() {
|
|
81
|
-
return Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
136
|
function getCategoryItems() {
|
|
85
137
|
return Array.from(document.querySelectorAll(SELECTORS.categoryItem));
|
|
86
138
|
}
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
function getPostContainers() {
|
|
141
|
+
var nodes = Array.from(document.querySelectorAll(SELECTORS.postItem));
|
|
142
|
+
return nodes.filter(function (el) {
|
|
143
|
+
if (!el || !el.isConnected) return false;
|
|
144
|
+
if (!el.querySelector('[component="post/content"]')) return false;
|
|
91
145
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const cfg = await res.json();
|
|
97
|
-
state.cfg = cfg;
|
|
98
|
-
return cfg;
|
|
99
|
-
} catch (e) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
})();
|
|
146
|
+
// Prevent nested / duplicated post wrappers.
|
|
147
|
+
var parentPost = el.parentElement && el.parentElement.closest('[component="post"][data-pid]');
|
|
148
|
+
if (parentPost && parentPost !== el) return false;
|
|
149
|
+
if (el.getAttribute('component') === 'post/parent') return false;
|
|
103
150
|
|
|
104
|
-
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
105
153
|
}
|
|
106
154
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
if (!state.poolPosts.length) state.poolPosts = parsePool(cfg.poolPosts);
|
|
110
|
-
if (!state.poolCategories.length) state.poolCategories = parsePool(cfg.poolCategories);
|
|
155
|
+
function safeRect(el) {
|
|
156
|
+
try { return el.getBoundingClientRect(); } catch (e) { return null; }
|
|
111
157
|
}
|
|
112
158
|
|
|
113
159
|
function destroyPlaceholderIds(ids) {
|
|
114
160
|
if (!ids || !ids.length) return;
|
|
115
|
-
|
|
161
|
+
|
|
162
|
+
// Only destroy ids that were actually "defined" (filled at least once) to avoid Ezoic warnings.
|
|
163
|
+
var filtered = ids.filter(function (id) {
|
|
164
|
+
try { return sessionDefinedIds.has(id); } catch (e) { return true; }
|
|
165
|
+
});
|
|
166
|
+
|
|
116
167
|
if (!filtered.length) return;
|
|
117
168
|
|
|
169
|
+
var call = function () {
|
|
170
|
+
try {
|
|
171
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') {
|
|
172
|
+
window.ezstandalone.destroyPlaceholders(filtered);
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {}
|
|
175
|
+
};
|
|
176
|
+
|
|
118
177
|
try {
|
|
119
178
|
window.ezstandalone = window.ezstandalone || {};
|
|
120
179
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
};
|
|
180
|
+
if (window.ezstandalone && typeof window.ezstandalone.destroyPlaceholders === 'function') call();
|
|
181
|
+
else window.ezstandalone.cmd.push(call);
|
|
182
|
+
} catch (e) {}
|
|
183
|
+
}
|
|
127
184
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
function getRecyclable(liveArr) {
|
|
186
|
+
var margin = 600; // px above viewport
|
|
187
|
+
for (var i = 0; i < liveArr.length; i++) {
|
|
188
|
+
var entry = liveArr[i];
|
|
189
|
+
if (!entry || !entry.wrap || !entry.wrap.isConnected) {
|
|
190
|
+
liveArr.splice(i, 1);
|
|
191
|
+
i--;
|
|
192
|
+
continue;
|
|
132
193
|
}
|
|
194
|
+
var r = safeRect(entry.wrap);
|
|
195
|
+
if (r && r.bottom < -margin) {
|
|
196
|
+
liveArr.splice(i, 1);
|
|
197
|
+
return entry;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
133
202
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
function pickId(pool, liveArr) {
|
|
204
|
+
if (pool.length) return { id: pool.shift(), recycled: null };
|
|
205
|
+
|
|
206
|
+
var recycled = getRecyclable(liveArr);
|
|
207
|
+
if (recycled) return { id: recycled.id, recycled: recycled };
|
|
208
|
+
|
|
209
|
+
return { id: null, recycled: null };
|
|
139
210
|
}
|
|
140
211
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
wrap.
|
|
144
|
-
wrap.setAttribute('data-ezoic-after', String(afterPos));
|
|
145
|
-
wrap.style.width = '100%';
|
|
212
|
+
function resetPlaceholderInWrap(wrap, id) {
|
|
213
|
+
if (!wrap) return null;
|
|
214
|
+
try { wrap.innerHTML = ''; } catch (e) {}
|
|
146
215
|
|
|
147
|
-
|
|
148
|
-
ph.id =
|
|
216
|
+
var ph = document.createElement('div');
|
|
217
|
+
ph.id = PLACEHOLDER_PREFIX + id;
|
|
149
218
|
wrap.appendChild(ph);
|
|
219
|
+
return ph;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isAdjacentAd(el) {
|
|
223
|
+
var next = el && el.nextElementSibling;
|
|
224
|
+
return !!(next && next.classList && next.classList.contains(WRAP_CLASS));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isPrevAd(el) {
|
|
228
|
+
var prev = el && el.previousElementSibling;
|
|
229
|
+
return !!(prev && prev.classList && prev.classList.contains(WRAP_CLASS));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildWrap(id, kindClass) {
|
|
233
|
+
var wrap = document.createElement('div');
|
|
234
|
+
wrap.className = WRAP_CLASS + ' ' + kindClass;
|
|
235
|
+
wrap.setAttribute('data-ezoic-id', String(id));
|
|
236
|
+
resetPlaceholderInWrap(wrap, id);
|
|
150
237
|
return wrap;
|
|
151
238
|
}
|
|
152
239
|
|
|
153
240
|
function findWrap(kindClass, afterPos) {
|
|
154
|
-
|
|
241
|
+
// Search a wrapper marker that we set on insertion.
|
|
242
|
+
return document.querySelector('.' + kindClass + '[data-after-pos="' + afterPos + '"]');
|
|
155
243
|
}
|
|
156
244
|
|
|
157
|
-
function insertAfter(
|
|
158
|
-
if (!target || !target.insertAdjacentElement) return null;
|
|
159
|
-
if (findWrap(kindClass, afterPos)) return null;
|
|
160
|
-
|
|
245
|
+
function insertAfter(el, id, kindClass, afterPos) {
|
|
161
246
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
247
|
+
var wrap = buildWrap(id, kindClass);
|
|
248
|
+
wrap.setAttribute('data-after-pos', String(afterPos));
|
|
249
|
+
|
|
250
|
+
if (!el || !el.parentNode) return null;
|
|
251
|
+
if (el.nextSibling) el.parentNode.insertBefore(wrap, el.nextSibling);
|
|
252
|
+
else el.parentNode.appendChild(wrap);
|
|
253
|
+
|
|
254
|
+
attachFillObserver(wrap, id);
|
|
164
255
|
return wrap;
|
|
165
|
-
} catch (e) {
|
|
166
|
-
|
|
167
|
-
}
|
|
256
|
+
} catch (e) {}
|
|
257
|
+
return null;
|
|
168
258
|
}
|
|
169
259
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
260
|
+
function destroyUsedPlaceholders() {
|
|
261
|
+
var ids = [];
|
|
262
|
+
state.usedTopics.forEach(function (id) { ids.push(id); });
|
|
263
|
+
state.usedPosts.forEach(function (id) { ids.push(id); });
|
|
264
|
+
state.usedCategories.forEach(function (id) { ids.push(id); });
|
|
265
|
+
|
|
266
|
+
// Only destroy placeholders that were filled at least once in this session.
|
|
267
|
+
destroyPlaceholderIds(ids);
|
|
173
268
|
}
|
|
174
269
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
270
|
+
function patchShowAds() {
|
|
271
|
+
// Some Ezoic setups require calling showAds via ezstandalone.cmd.
|
|
272
|
+
// We keep existing behavior but make it resilient.
|
|
178
273
|
try {
|
|
179
274
|
window.ezstandalone = window.ezstandalone || {};
|
|
180
275
|
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
276
|
+
} catch (e) {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function markFilled(id) {
|
|
280
|
+
try { sessionDefinedIds.add(id); } catch (e) {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isWrapMarkedFilled(wrap) {
|
|
284
|
+
try { return !!(wrap && wrap.getAttribute && wrap.getAttribute('data-ezoic-filled') === '1'); } catch (e) { return false; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function attachFillObserver(wrap, id) {
|
|
288
|
+
if (!wrap || !wrap.isConnected) return;
|
|
289
|
+
|
|
290
|
+
// If already filled, mark and return.
|
|
291
|
+
if (isPlaceholderFilled(wrap)) {
|
|
292
|
+
try { wrap.setAttribute('data-ezoic-filled', '1'); } catch (e) {}
|
|
293
|
+
markFilled(id);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Observe for Ezoic inserting ad content into placeholder.
|
|
298
|
+
try {
|
|
299
|
+
var obs = new MutationObserver(function () {
|
|
300
|
+
if (!wrap.isConnected) {
|
|
301
|
+
try { obs.disconnect(); } catch (e) {}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (isPlaceholderFilled(wrap)) {
|
|
305
|
+
try { wrap.setAttribute('data-ezoic-filled', '1'); } catch (e) {}
|
|
306
|
+
markFilled(id);
|
|
307
|
+
try { obs.disconnect(); } catch (e) {}
|
|
186
308
|
}
|
|
187
309
|
});
|
|
310
|
+
obs.observe(wrap, { childList: true, subtree: true });
|
|
311
|
+
wrap.__ezoicFillObs = obs;
|
|
188
312
|
} catch (e) {}
|
|
189
313
|
}
|
|
190
314
|
|
|
191
|
-
function
|
|
192
|
-
|
|
315
|
+
function isPlaceholderFilled(wrap) {
|
|
316
|
+
// Heuristic: placeholder exists AND has descendants or meaningful height.
|
|
317
|
+
try {
|
|
318
|
+
var ph = wrap.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
319
|
+
if (!ph) return false;
|
|
320
|
+
if (ph.children && ph.children.length) return true;
|
|
321
|
+
var r = safeRect(wrap);
|
|
322
|
+
if (r && r.height > 20) return true;
|
|
323
|
+
} catch (e) {}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
193
326
|
|
|
194
|
-
|
|
195
|
-
|
|
327
|
+
function scheduleShowAdsBatch(ids) {
|
|
328
|
+
if (!ids || !ids.length) return;
|
|
196
329
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
330
|
+
// Ezoic expects DOM to be settled.
|
|
331
|
+
var call = function () {
|
|
332
|
+
try {
|
|
333
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
334
|
+
window.ezstandalone.showAds(ids);
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') call();
|
|
341
|
+
else if (window.ezstandalone && window.ezstandalone.cmd && Array.isArray(window.ezstandalone.cmd)) window.ezstandalone.cmd.push(call);
|
|
342
|
+
} catch (e) {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function callShowAdsWhenReady(id) {
|
|
346
|
+
if (!id) return;
|
|
347
|
+
|
|
348
|
+
// Throttle per-id.
|
|
349
|
+
var now = Date.now();
|
|
350
|
+
var last = state.lastShowById.get(id) || 0;
|
|
351
|
+
if (now - last < 1200) return;
|
|
352
|
+
|
|
353
|
+
if (state.pendingById.has(id)) return;
|
|
354
|
+
state.pendingById.add(id);
|
|
355
|
+
state.lastShowById.set(id, now);
|
|
356
|
+
|
|
357
|
+
// Guard against re-entrancy.
|
|
358
|
+
if (insertingIds.has(id)) {
|
|
359
|
+
state.pendingById.delete(id);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
insertingIds.add(id);
|
|
363
|
+
|
|
364
|
+
var attempts = 0;
|
|
365
|
+
|
|
366
|
+
(function waitForPh() {
|
|
367
|
+
attempts++;
|
|
368
|
+
|
|
369
|
+
// Navigation safety: if we navigated away, stop.
|
|
370
|
+
if (!state.canShowAds) {
|
|
371
|
+
state.pendingById.delete(id);
|
|
372
|
+
insertingIds.delete(id);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
var ph = document.getElementById(PLACEHOLDER_PREFIX + id);
|
|
377
|
+
|
|
378
|
+
var doCall = function () {
|
|
379
|
+
try {
|
|
380
|
+
// If placeholder is gone, stop.
|
|
381
|
+
if (!ph || !ph.isConnected) return false;
|
|
382
|
+
scheduleShowAdsBatch([id]);
|
|
383
|
+
return true;
|
|
384
|
+
} catch (e) {}
|
|
385
|
+
return false;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
if (ph && ph.isConnected) {
|
|
389
|
+
doCall();
|
|
390
|
+
state.pendingById.delete(id);
|
|
391
|
+
insertingIds.delete(id);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (attempts < 100) {
|
|
396
|
+
setTimeoutTracked(waitForPh, 50);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Timeout: give up silently.
|
|
401
|
+
state.pendingById.delete(id);
|
|
402
|
+
insertingIds.delete(id);
|
|
403
|
+
})();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function initPools(cfg) {
|
|
407
|
+
if (!state.poolTopics.length) state.poolTopics = parsePool(cfg.placeholderIds);
|
|
408
|
+
if (!state.poolPosts.length) state.poolPosts = parsePool(cfg.messagePlaceholderIds);
|
|
409
|
+
if (!state.poolCategories.length) state.poolCategories = parsePool(cfg.categoryPlaceholderIds);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function computeTargets(count, interval, showFirst) {
|
|
413
|
+
var out = [];
|
|
414
|
+
if (count <= 0) return out;
|
|
415
|
+
|
|
416
|
+
if (showFirst) out.push(1);
|
|
417
|
+
|
|
418
|
+
for (var i = 1; i <= count; i++) {
|
|
419
|
+
if (i % interval === 0) out.push(i);
|
|
202
420
|
}
|
|
203
421
|
|
|
204
|
-
|
|
422
|
+
// Unique + sorted.
|
|
423
|
+
return Array.from(new Set(out)).sort(function (a, b) { return a - b; });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function injectBetween(kindClass, items, interval, showFirst, kindPool, usedSet, liveArr) {
|
|
427
|
+
if (!items || !items.length) return 0;
|
|
428
|
+
|
|
429
|
+
var targets = computeTargets(items.length, interval, showFirst);
|
|
430
|
+
var inserted = 0;
|
|
431
|
+
|
|
432
|
+
for (var t = 0; t < targets.length; t++) {
|
|
433
|
+
var afterPos = targets[t];
|
|
205
434
|
if (inserted >= MAX_INSERTS_PER_RUN) break;
|
|
206
435
|
|
|
207
|
-
|
|
436
|
+
var el = items[afterPos - 1];
|
|
208
437
|
if (!el || !el.isConnected) continue;
|
|
438
|
+
|
|
439
|
+
// Prevent adjacent ads.
|
|
440
|
+
if (isAdjacentAd(el) || isPrevAd(el)) continue;
|
|
441
|
+
|
|
442
|
+
// Prevent duplicates at same logical position.
|
|
443
|
+
if (findWrap(kindClass, afterPos - 1)) continue;
|
|
209
444
|
if (findWrap(kindClass, afterPos)) continue;
|
|
210
445
|
|
|
211
|
-
|
|
446
|
+
var pick = pickId(kindPool, liveArr);
|
|
447
|
+
var id = pick.id;
|
|
212
448
|
if (!id) break;
|
|
213
449
|
|
|
214
|
-
|
|
215
|
-
if (!wrap) continue;
|
|
450
|
+
var wrap = null;
|
|
216
451
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
452
|
+
if (pick.recycled && pick.recycled.wrap) {
|
|
453
|
+
// Recycle: only destroy if Ezoic has actually defined this placeholder before.
|
|
454
|
+
if (sessionDefinedIds.has(id)) destroyPlaceholderIds([id]);
|
|
455
|
+
|
|
456
|
+
// Remove old wrapper.
|
|
457
|
+
var oldWrap = pick.recycled.wrap;
|
|
458
|
+
try { if (oldWrap && oldWrap.__ezoicFillObs) oldWrap.__ezoicFillObs.disconnect(); } catch (e) {}
|
|
459
|
+
try { if (oldWrap) oldWrap.remove(); } catch (e) {}
|
|
460
|
+
|
|
461
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
462
|
+
if (!wrap) continue;
|
|
463
|
+
|
|
464
|
+
// Give Ezoic a moment after DOM insertion.
|
|
465
|
+
setTimeoutTracked(function () { callShowAdsWhenReady(id); }, 700);
|
|
466
|
+
} else {
|
|
467
|
+
usedSet.add(id);
|
|
468
|
+
wrap = insertAfter(el, id, kindClass, afterPos);
|
|
469
|
+
if (!wrap) continue;
|
|
470
|
+
|
|
471
|
+
// Micro-delay to allow layout/DOM settle.
|
|
472
|
+
setTimeoutTracked(function () { callShowAdsWhenReady(id); }, 10);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
liveArr.push({ id: id, wrap: wrap });
|
|
476
|
+
|
|
477
|
+
// Final safety: if adjacency happened due to DOM shifts, rollback.
|
|
478
|
+
var prev = wrap && wrap.previousElementSibling;
|
|
479
|
+
var next = wrap && wrap.nextElementSibling;
|
|
480
|
+
if (wrap && ((prev && prev.classList && prev.classList.contains(WRAP_CLASS)) || (next && next.classList && next.classList.contains(WRAP_CLASS)))) {
|
|
481
|
+
try { wrap.remove(); } catch (e) {}
|
|
482
|
+
|
|
483
|
+
if (!(pick.recycled && pick.recycled.wrap)) {
|
|
484
|
+
try { kindPool.unshift(id); } catch (e) {}
|
|
485
|
+
usedSet.delete(id);
|
|
486
|
+
}
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
inserted++;
|
|
220
491
|
}
|
|
221
492
|
|
|
222
493
|
return inserted;
|
|
223
494
|
}
|
|
224
495
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
} else if (kind === 'categoryTopics' && normalizeBool(cfg.enableBetweenAds)) {
|
|
246
|
-
inserted = injectBetween(
|
|
247
|
-
'ezoic-ad-between',
|
|
248
|
-
getTopicItems(),
|
|
249
|
-
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
250
|
-
normalizeBool(cfg.showFirstTopicAd),
|
|
251
|
-
state.poolTopics,
|
|
252
|
-
state.usedTopics
|
|
253
|
-
);
|
|
254
|
-
} else if (kind === 'categories' && normalizeBool(cfg.enableCategoryAds)) {
|
|
255
|
-
inserted = injectBetween(
|
|
256
|
-
'ezoic-ad-category',
|
|
257
|
-
getCategoryItems(),
|
|
258
|
-
Math.max(1, parseInt(cfg.intervalCategories, 10) || 6),
|
|
259
|
-
normalizeBool(cfg.showFirstCategoryAd),
|
|
260
|
-
state.poolCategories,
|
|
261
|
-
state.usedCategories
|
|
262
|
-
);
|
|
496
|
+
function enforceNoAdjacentAds() {
|
|
497
|
+
var ads = Array.from(document.querySelectorAll('.' + WRAP_CLASS));
|
|
498
|
+
for (var i = 0; i < ads.length; i++) {
|
|
499
|
+
var ad = ads[i];
|
|
500
|
+
var prev = ad.previousElementSibling;
|
|
501
|
+
|
|
502
|
+
if (prev && prev.classList && prev.classList.contains(WRAP_CLASS)) {
|
|
503
|
+
// Remove adjacent wrapper (do not hide).
|
|
504
|
+
try {
|
|
505
|
+
var ph = ad.querySelector('[id^="' + PLACEHOLDER_PREFIX + '"]');
|
|
506
|
+
if (ph) {
|
|
507
|
+
var id = parseInt(ph.id.replace(PLACEHOLDER_PREFIX, ''), 10);
|
|
508
|
+
if (Number.isFinite(id) && id > 0 && sessionDefinedIds.has(id)) {
|
|
509
|
+
destroyPlaceholderIds([id]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
ad.remove();
|
|
513
|
+
} catch (e) {}
|
|
514
|
+
}
|
|
263
515
|
}
|
|
264
516
|
}
|
|
265
517
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
state.
|
|
518
|
+
function cleanup() {
|
|
519
|
+
// Stop any insertion during navigation / DOM teardown.
|
|
520
|
+
state.canShowAds = false;
|
|
521
|
+
state.poolWaitAttempts = 0;
|
|
522
|
+
state.awaitItemsAttempts = 0;
|
|
269
523
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
state.scheduled = false;
|
|
273
|
-
const pk = getPageKey();
|
|
274
|
-
if (state.pageKey && pk !== state.pageKey) return;
|
|
275
|
-
runCore().catch(() => {});
|
|
276
|
-
}, 50);
|
|
277
|
-
}
|
|
524
|
+
// Cancel any pending showAds timeouts.
|
|
525
|
+
clearAllTrackedTimeouts();
|
|
278
526
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
527
|
+
// Disconnect global observer to avoid mutations during teardown.
|
|
528
|
+
if (state.obs) {
|
|
529
|
+
try { state.obs.disconnect(); } catch (e) {}
|
|
530
|
+
state.obs = null;
|
|
531
|
+
}
|
|
282
532
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
});
|
|
533
|
+
// Destroy placeholders that were used (only those that were actually defined).
|
|
534
|
+
destroyUsedPlaceholders();
|
|
286
535
|
|
|
536
|
+
// Remove wrappers from DOM (safe because insertion is now blocked).
|
|
537
|
+
try {
|
|
538
|
+
document.querySelectorAll('.' + WRAP_CLASS).forEach(function (el) {
|
|
539
|
+
try { if (el && el.__ezoicFillObs) el.__ezoicFillObs.disconnect(); } catch (e) {}
|
|
540
|
+
try { el.remove(); } catch (e) {}
|
|
541
|
+
});
|
|
542
|
+
} catch (e) {}
|
|
543
|
+
|
|
544
|
+
// Reset runtime caches.
|
|
287
545
|
state.pageKey = getPageKey();
|
|
288
546
|
state.cfg = null;
|
|
289
547
|
state.cfgPromise = null;
|
|
548
|
+
|
|
290
549
|
state.poolTopics = [];
|
|
291
550
|
state.poolPosts = [];
|
|
292
551
|
state.poolCategories = [];
|
|
552
|
+
|
|
293
553
|
state.usedTopics.clear();
|
|
294
554
|
state.usedPosts.clear();
|
|
295
555
|
state.usedCategories.clear();
|
|
296
|
-
state.lastShowById.clear();
|
|
297
|
-
sessionDefinedIds.clear();
|
|
298
556
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
557
|
+
state.liveTopics = [];
|
|
558
|
+
state.livePosts = [];
|
|
559
|
+
state.liveCategories = [];
|
|
560
|
+
|
|
561
|
+
state.lastShowById.clear();
|
|
562
|
+
state.pendingById.clear();
|
|
563
|
+
insertingIds.clear();
|
|
303
564
|
|
|
304
565
|
state.scheduled = false;
|
|
305
|
-
|
|
306
|
-
|
|
566
|
+
if (state.timer) {
|
|
567
|
+
try { clearTimeout(state.timer); } catch (e) {}
|
|
568
|
+
state.timer = null;
|
|
569
|
+
}
|
|
307
570
|
}
|
|
308
571
|
|
|
309
572
|
function ensureObserver() {
|
|
310
573
|
if (state.obs) return;
|
|
311
|
-
state.obs = new MutationObserver(() => scheduleRun());
|
|
312
574
|
try {
|
|
575
|
+
state.obs = new MutationObserver(function () { scheduleRun('mutation'); });
|
|
313
576
|
state.obs.observe(document.body, { childList: true, subtree: true });
|
|
314
577
|
} catch (e) {}
|
|
315
578
|
}
|
|
316
579
|
|
|
580
|
+
function scheduleRun(/* reason */) {
|
|
581
|
+
if (state.scheduled) return;
|
|
582
|
+
state.scheduled = true;
|
|
583
|
+
|
|
584
|
+
if (state.timer) {
|
|
585
|
+
try { clearTimeout(state.timer); } catch (e) {}
|
|
586
|
+
state.timer = null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
state.timer = setTimeoutTracked(function () {
|
|
590
|
+
state.scheduled = false;
|
|
591
|
+
|
|
592
|
+
// If user navigated away, stop.
|
|
593
|
+
var pk = getPageKey();
|
|
594
|
+
if (state.pageKey && pk !== state.pageKey) return;
|
|
595
|
+
|
|
596
|
+
runCore().catch(function () {});
|
|
597
|
+
}, 80);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function waitForItemsThenRun(kind) {
|
|
601
|
+
// If list isn't in DOM yet (ajaxify transition), retry a bit.
|
|
602
|
+
var count = 0;
|
|
603
|
+
if (kind === 'topic') count = getPostContainers().length;
|
|
604
|
+
else if (kind === 'categoryTopics') count = getTopicItems().length;
|
|
605
|
+
else if (kind === 'categories') count = getCategoryItems().length;
|
|
606
|
+
|
|
607
|
+
if (count > 0) return true;
|
|
608
|
+
|
|
609
|
+
if (state.awaitItemsAttempts < 25) {
|
|
610
|
+
state.awaitItemsAttempts++;
|
|
611
|
+
setTimeoutTracked(function () { scheduleRun('await-items'); }, 120);
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
|
|
317
616
|
function waitForContentThenRun() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
617
|
+
// Avoid inserting ads on pages with too little content.
|
|
618
|
+
var MIN_WORDS = 250;
|
|
619
|
+
var attempts = 0;
|
|
620
|
+
var maxAttempts = 20; // 20 × 200ms = 4s
|
|
621
|
+
|
|
622
|
+
(function check() {
|
|
623
|
+
attempts++;
|
|
624
|
+
|
|
625
|
+
var text = '';
|
|
626
|
+
try { text = document.body.innerText || ''; } catch (e) {}
|
|
627
|
+
var wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
628
|
+
|
|
629
|
+
if (wordCount >= MIN_WORDS) {
|
|
630
|
+
scheduleRun('content-ok');
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (attempts >= maxAttempts) {
|
|
635
|
+
scheduleRun('content-timeout');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
setTimeoutTracked(check, 200);
|
|
640
|
+
})();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function waitForEzoicThenRun() {
|
|
644
|
+
var attempts = 0;
|
|
645
|
+
var maxAttempts = 50; // 50 × 200ms = 10s
|
|
646
|
+
|
|
647
|
+
(function check() {
|
|
648
|
+
attempts++;
|
|
649
|
+
|
|
650
|
+
if (window.ezstandalone && typeof window.ezstandalone.showAds === 'function') {
|
|
651
|
+
scheduleRun('ezoic-ready');
|
|
652
|
+
waitForContentThenRun();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (attempts >= maxAttempts) {
|
|
657
|
+
scheduleRun('ezoic-timeout');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
setTimeoutTracked(check, 200);
|
|
662
|
+
})();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function fetchConfig() {
|
|
666
|
+
if (state.cfg) return Promise.resolve(state.cfg);
|
|
667
|
+
if (state.cfgPromise) return state.cfgPromise;
|
|
668
|
+
|
|
669
|
+
state.cfgPromise = (function () {
|
|
670
|
+
var MAX_TRIES = 3;
|
|
671
|
+
var delay = 800;
|
|
672
|
+
|
|
673
|
+
function attemptFetch(attempt) {
|
|
674
|
+
return fetch('/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' })
|
|
675
|
+
.then(function (res) {
|
|
676
|
+
if (!res || !res.ok) throw new Error('bad response');
|
|
677
|
+
return res.json();
|
|
678
|
+
})
|
|
679
|
+
.then(function (json) {
|
|
680
|
+
state.cfg = json;
|
|
681
|
+
return json;
|
|
682
|
+
})
|
|
683
|
+
.catch(function () {
|
|
684
|
+
if (attempt >= MAX_TRIES) return null;
|
|
685
|
+
return new Promise(function (r) { setTimeoutTracked(r, delay); }).then(function () {
|
|
686
|
+
delay *= 2;
|
|
687
|
+
return attemptFetch(attempt + 1);
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return attemptFetch(1).finally(function () { state.cfgPromise = null; });
|
|
693
|
+
})();
|
|
694
|
+
|
|
695
|
+
return state.cfgPromise;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function runCore() {
|
|
699
|
+
// Navigation safety: never insert during ajaxify teardown.
|
|
700
|
+
if (!state.canShowAds) return Promise.resolve();
|
|
701
|
+
|
|
702
|
+
patchShowAds();
|
|
703
|
+
|
|
704
|
+
return fetchConfig().then(function (cfg) {
|
|
705
|
+
if (!cfg || cfg.excluded) return;
|
|
706
|
+
|
|
707
|
+
initPools(cfg);
|
|
708
|
+
|
|
709
|
+
var kind = getKind();
|
|
710
|
+
var inserted = 0;
|
|
711
|
+
|
|
712
|
+
if (!waitForItemsThenRun(kind)) return;
|
|
713
|
+
|
|
714
|
+
if (kind === 'topic') {
|
|
715
|
+
if (normalizeBool(cfg.enableMessageAds)) {
|
|
716
|
+
inserted = injectBetween(
|
|
717
|
+
'ezoic-ad-message',
|
|
718
|
+
getPostContainers(),
|
|
719
|
+
Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3),
|
|
720
|
+
normalizeBool(cfg.showFirstMessageAd),
|
|
721
|
+
state.poolPosts,
|
|
722
|
+
state.usedPosts,
|
|
723
|
+
state.livePosts
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
} else if (kind === 'categoryTopics') {
|
|
727
|
+
if (normalizeBool(cfg.enableBetweenAds)) {
|
|
728
|
+
inserted = injectBetween(
|
|
729
|
+
'ezoic-ad-between',
|
|
730
|
+
getTopicItems(),
|
|
731
|
+
Math.max(1, parseInt(cfg.intervalPosts, 10) || 6),
|
|
732
|
+
normalizeBool(cfg.showFirstTopicAd),
|
|
733
|
+
state.poolTopics,
|
|
734
|
+
state.usedTopics,
|
|
735
|
+
state.liveTopics
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
} else if (kind === 'categories') {
|
|
739
|
+
if (normalizeBool(cfg.enableCategoryAds)) {
|
|
740
|
+
inserted = injectBetween(
|
|
741
|
+
'ezoic-ad-categories',
|
|
742
|
+
getCategoryItems(),
|
|
743
|
+
Math.max(1, parseInt(cfg.intervalCategories, 10) || 4),
|
|
744
|
+
normalizeBool(cfg.showFirstCategoryAd),
|
|
745
|
+
state.poolCategories,
|
|
746
|
+
state.usedCategories,
|
|
747
|
+
state.liveCategories
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
enforceNoAdjacentAds();
|
|
753
|
+
|
|
754
|
+
// Recycling: if pool is exhausted, retry a few times to allow old wrappers to scroll off-screen.
|
|
755
|
+
if (inserted === 0) {
|
|
756
|
+
if (state.poolWaitAttempts < 8) {
|
|
757
|
+
state.poolWaitAttempts++;
|
|
758
|
+
setTimeoutTracked(function () { scheduleRun('pool-wait'); }, 400);
|
|
759
|
+
}
|
|
326
760
|
} else {
|
|
327
|
-
|
|
761
|
+
// Reset pool wait attempts once we successfully insert something.
|
|
762
|
+
state.poolWaitAttempts = 0;
|
|
328
763
|
}
|
|
329
|
-
|
|
330
|
-
|
|
764
|
+
|
|
765
|
+
// If we hit max inserts, continue quickly.
|
|
766
|
+
if (inserted >= MAX_INSERTS_PER_RUN) {
|
|
767
|
+
setTimeoutTracked(function () { scheduleRun('continue'); }, 140);
|
|
768
|
+
}
|
|
769
|
+
}).catch(function () {});
|
|
331
770
|
}
|
|
332
771
|
|
|
333
772
|
function bind() {
|
|
334
773
|
if (!$) return;
|
|
335
774
|
|
|
336
775
|
$(window).off('.ezoicInfinite');
|
|
337
|
-
|
|
338
|
-
$(window).on('action:ajaxify.
|
|
776
|
+
|
|
777
|
+
$(window).on('action:ajaxify.start.ezoicInfinite', function () {
|
|
778
|
+
cleanup();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
$(window).on('action:ajaxify.end.ezoicInfinite', function () {
|
|
339
782
|
state.pageKey = getPageKey();
|
|
340
783
|
ensureObserver();
|
|
341
|
-
|
|
342
|
-
|
|
784
|
+
|
|
785
|
+
// Delay gate to avoid racing NodeBB DOM swap vs Ezoic processing.
|
|
786
|
+
setTimeoutTracked(function () {
|
|
787
|
+
state.canShowAds = true;
|
|
788
|
+
waitForEzoicThenRun();
|
|
789
|
+
}, 300);
|
|
343
790
|
});
|
|
344
791
|
|
|
345
|
-
|
|
792
|
+
// Infinite-scroll and "loaded" events.
|
|
793
|
+
$(window).on('action:category.loaded.ezoicInfinite', function () {
|
|
346
794
|
ensureObserver();
|
|
347
795
|
waitForContentThenRun();
|
|
348
796
|
});
|
|
349
797
|
|
|
350
|
-
$(window).on('action:topics.loaded.ezoicInfinite', ()
|
|
798
|
+
$(window).on('action:topics.loaded.ezoicInfinite', function () {
|
|
351
799
|
ensureObserver();
|
|
352
800
|
waitForContentThenRun();
|
|
353
801
|
});
|
|
354
|
-
}
|
|
355
802
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
803
|
+
$(window).on('action:topic.loaded.ezoicInfinite', function () {
|
|
804
|
+
ensureObserver();
|
|
805
|
+
waitForContentThenRun();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
$(window).on('action:posts.loaded.ezoicInfinite', function () {
|
|
809
|
+
ensureObserver();
|
|
810
|
+
waitForContentThenRun();
|
|
811
|
+
});
|
|
362
812
|
}
|
|
363
813
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
814
|
+
function bindScroll() {
|
|
815
|
+
if (state.lastScrollRun > 0) return;
|
|
816
|
+
state.lastScrollRun = Date.now();
|
|
817
|
+
|
|
818
|
+
var ticking = false;
|
|
819
|
+
window.addEventListener('scroll', function () {
|
|
820
|
+
if (ticking) return;
|
|
821
|
+
ticking = true;
|
|
822
|
+
|
|
823
|
+
window.requestAnimationFrame(function () {
|
|
824
|
+
ticking = false;
|
|
825
|
+
|
|
826
|
+
enforceNoAdjacentAds();
|
|
827
|
+
|
|
828
|
+
// Debounce scheduleRun (max once every 2s on scroll).
|
|
829
|
+
var now = Date.now();
|
|
830
|
+
if (!state.lastScrollRun || (now - state.lastScrollRun > 2000)) {
|
|
831
|
+
state.lastScrollRun = now;
|
|
832
|
+
scheduleRun('scroll');
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}, { passive: true });
|
|
370
836
|
}
|
|
837
|
+
|
|
838
|
+
// Boot.
|
|
839
|
+
cleanup();
|
|
840
|
+
bind();
|
|
841
|
+
bindScroll();
|
|
842
|
+
ensureObserver();
|
|
843
|
+
|
|
844
|
+
state.pageKey = getPageKey();
|
|
845
|
+
|
|
846
|
+
// Direct page load: allow insertion after initial tick (no ajaxify.end).
|
|
847
|
+
setTimeoutTracked(function () {
|
|
848
|
+
state.canShowAds = true;
|
|
849
|
+
waitForEzoicThenRun();
|
|
850
|
+
}, 0);
|
|
371
851
|
})();
|
package/public/style.css
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
/* Keep Ezoic wrappers “CLS-safe” and remove the extra vertical spacing Ezoic often injects */
|
|
1
2
|
.ezoic-ad {
|
|
3
|
+
display: block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
margin: 0 !important;
|
|
2
6
|
padding: 0 !important;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ezoic-ad > [id^="ezoic-pub-ad-placeholder-"] {
|
|
3
11
|
margin: 0 !important;
|
|
4
|
-
|
|
5
|
-
min-
|
|
12
|
+
padding: 0 !important;
|
|
13
|
+
min-height: 1px; /* keeps placeholder measurable for IO */
|
|
6
14
|
}
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
/* Ezoic sometimes wraps in extra spans/divs with margins */
|
|
17
|
+
.ezoic-ad span.ezoic-ad,
|
|
18
|
+
.ezoic-ad .ezoic-ad {
|
|
19
|
+
margin: 0 !important;
|
|
20
|
+
padding: 0 !important;
|
|
11
21
|
}
|